@agentick/client 0.0.1

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/dist/client.js ADDED
@@ -0,0 +1,1381 @@
1
+ /**
2
+ * AgentickClient - Multiplexed session client
3
+ *
4
+ * Connects to a Agentick server with a single SSE connection
5
+ * that multiplexes events for multiple sessions.
6
+ *
7
+ * @module @agentick/client
8
+ */
9
+ // ============================================================================
10
+ // Channel Accessor Implementation
11
+ // ============================================================================
12
+ class ChannelAccessorImpl {
13
+ sendFn;
14
+ name;
15
+ handlers = new Set();
16
+ pendingRequests = new Map();
17
+ constructor(name, sendFn) {
18
+ this.sendFn = sendFn;
19
+ this.name = name;
20
+ }
21
+ subscribe(handler) {
22
+ this.handlers.add(handler);
23
+ return () => {
24
+ this.handlers.delete(handler);
25
+ };
26
+ }
27
+ async publish(type, payload) {
28
+ await this.sendFn({
29
+ channel: this.name,
30
+ type,
31
+ payload,
32
+ metadata: { timestamp: Date.now() },
33
+ });
34
+ }
35
+ async request(type, payload, timeoutMs = 30000) {
36
+ const requestId = generateId();
37
+ return new Promise((resolve, reject) => {
38
+ const timeout = setTimeout(() => {
39
+ this.pendingRequests.delete(requestId);
40
+ reject(new Error(`Request timed out after ${timeoutMs}ms`));
41
+ }, timeoutMs);
42
+ this.pendingRequests.set(requestId, {
43
+ resolve: resolve,
44
+ reject,
45
+ timeout,
46
+ });
47
+ this.sendFn({
48
+ channel: this.name,
49
+ type,
50
+ payload,
51
+ id: requestId,
52
+ metadata: { timestamp: Date.now() },
53
+ }).catch((error) => {
54
+ this.pendingRequests.delete(requestId);
55
+ clearTimeout(timeout);
56
+ reject(error);
57
+ });
58
+ });
59
+ }
60
+ /** @internal */
61
+ _handleEvent(event) {
62
+ // Check for response to pending request
63
+ if (event.type === "response" && event.id) {
64
+ const pending = this.pendingRequests.get(event.id);
65
+ if (pending) {
66
+ clearTimeout(pending.timeout);
67
+ this.pendingRequests.delete(event.id);
68
+ pending.resolve(event.payload);
69
+ return;
70
+ }
71
+ }
72
+ // Check for error response
73
+ if (event.type === "error" && event.id) {
74
+ const pending = this.pendingRequests.get(event.id);
75
+ if (pending) {
76
+ clearTimeout(pending.timeout);
77
+ this.pendingRequests.delete(event.id);
78
+ pending.reject(new Error(event.payload?.message ?? "Request failed"));
79
+ return;
80
+ }
81
+ }
82
+ // Notify subscribers
83
+ for (const handler of this.handlers) {
84
+ try {
85
+ handler(event.payload, event);
86
+ }
87
+ catch (error) {
88
+ console.error(`Error in channel handler for ${this.name}:`, error);
89
+ }
90
+ }
91
+ }
92
+ /** @internal */
93
+ _destroy() {
94
+ for (const [, pending] of this.pendingRequests) {
95
+ clearTimeout(pending.timeout);
96
+ pending.reject(new Error("Channel destroyed"));
97
+ }
98
+ this.pendingRequests.clear();
99
+ this.handlers.clear();
100
+ }
101
+ }
102
+ // ============================================================================
103
+ // Async Event Queue (single-consumer)
104
+ // ============================================================================
105
+ class AsyncEventQueue {
106
+ buffer = [];
107
+ resolvers = [];
108
+ closed = false;
109
+ push(value) {
110
+ if (this.closed)
111
+ return;
112
+ const resolver = this.resolvers.shift();
113
+ if (resolver) {
114
+ resolver({ value, done: false });
115
+ return;
116
+ }
117
+ this.buffer.push(value);
118
+ }
119
+ close() {
120
+ if (this.closed)
121
+ return;
122
+ this.closed = true;
123
+ while (this.resolvers.length > 0) {
124
+ const resolver = this.resolvers.shift();
125
+ if (resolver) {
126
+ resolver({ value: undefined, done: true });
127
+ }
128
+ }
129
+ }
130
+ [Symbol.asyncIterator]() {
131
+ return {
132
+ next: () => {
133
+ if (this.buffer.length > 0) {
134
+ const value = this.buffer.shift();
135
+ return Promise.resolve({ value, done: false });
136
+ }
137
+ if (this.closed) {
138
+ return Promise.resolve({ value: undefined, done: true });
139
+ }
140
+ return new Promise((resolve) => {
141
+ this.resolvers.push(resolve);
142
+ });
143
+ },
144
+ };
145
+ }
146
+ }
147
+ // ============================================================================
148
+ // Client Execution Handle
149
+ // ============================================================================
150
+ class ClientExecutionHandleImpl {
151
+ client;
152
+ abortController;
153
+ queue = new AsyncEventQueue();
154
+ resultPromise;
155
+ resolveResult;
156
+ rejectResult;
157
+ _status = "running";
158
+ _sessionId;
159
+ _executionId = "pending";
160
+ hasResult = false;
161
+ constructor(client, abortController, sessionId) {
162
+ this.client = client;
163
+ this.abortController = abortController;
164
+ this._sessionId = sessionId ?? "pending";
165
+ this.resultPromise = new Promise((resolve, reject) => {
166
+ this.resolveResult = resolve;
167
+ this.rejectResult = reject;
168
+ });
169
+ }
170
+ get sessionId() {
171
+ return this._sessionId;
172
+ }
173
+ get executionId() {
174
+ return this._executionId;
175
+ }
176
+ get status() {
177
+ return this._status;
178
+ }
179
+ get result() {
180
+ return this.resultPromise;
181
+ }
182
+ [Symbol.asyncIterator]() {
183
+ return this.queue[Symbol.asyncIterator]();
184
+ }
185
+ abort(reason) {
186
+ if (this._status !== "running")
187
+ return;
188
+ this._status = "aborted";
189
+ this.abortController.abort(reason ?? "Client aborted execution");
190
+ if (this._sessionId !== "pending") {
191
+ void this.client.abort(this._sessionId, reason).catch(() => { });
192
+ }
193
+ this.queue.close();
194
+ this.rejectResult(new Error(reason ?? "Execution aborted"));
195
+ }
196
+ queueMessage(message) {
197
+ if (this._sessionId === "pending") {
198
+ throw new Error("Cannot queue message before sessionId is known");
199
+ }
200
+ const handle = this.client.send({ messages: [message] }, { sessionId: this._sessionId });
201
+ void handle.result.catch(() => { });
202
+ }
203
+ submitToolResult(toolUseId, result) {
204
+ if (this._sessionId === "pending") {
205
+ throw new Error("Cannot submit tool result before sessionId is known");
206
+ }
207
+ void this.client.submitToolResult(this._sessionId, toolUseId, result).catch(() => { });
208
+ }
209
+ /** @internal */
210
+ _handleEvent(event) {
211
+ if (event.sessionId) {
212
+ this._sessionId = event.sessionId;
213
+ }
214
+ if ("executionId" in event && event.executionId) {
215
+ this._executionId = event.executionId;
216
+ }
217
+ const streamEvent = event;
218
+ this.queue.push(streamEvent);
219
+ if (event.type === "result") {
220
+ this.hasResult = true;
221
+ this._status = "completed";
222
+ this.resolveResult(event.result);
223
+ }
224
+ // Handle server-side errors
225
+ if (event.type === "error") {
226
+ this.hasResult = true; // Prevent "completed without result" error
227
+ this._status = "error";
228
+ const errorMessage = event.error ?? "Server error";
229
+ this.rejectResult(new Error(errorMessage));
230
+ }
231
+ if (event.type === "execution_end") {
232
+ if (this._status === "running") {
233
+ this._status = "completed";
234
+ }
235
+ this.queue.close();
236
+ }
237
+ }
238
+ /** @internal */
239
+ _fail(error) {
240
+ if (this._status === "running") {
241
+ this._status = "error";
242
+ }
243
+ this.queue.close();
244
+ if (!this.hasResult) {
245
+ this.rejectResult(error);
246
+ }
247
+ }
248
+ /** @internal */
249
+ _complete() {
250
+ if (this._status === "running") {
251
+ this._status = "completed";
252
+ }
253
+ this.queue.close();
254
+ if (!this.hasResult) {
255
+ this.rejectResult(new Error("Execution completed without result"));
256
+ }
257
+ }
258
+ }
259
+ // ============================================================================
260
+ // Session Accessor Implementation
261
+ // ============================================================================
262
+ class SessionAccessorImpl {
263
+ client;
264
+ sessionId;
265
+ _isSubscribed = false;
266
+ eventHandlers = new Set();
267
+ resultHandlers = new Set();
268
+ toolConfirmationHandlers = new Set();
269
+ channels = new Map();
270
+ constructor(sessionId, client) {
271
+ this.client = client;
272
+ this.sessionId = sessionId;
273
+ }
274
+ get isSubscribed() {
275
+ return this._isSubscribed;
276
+ }
277
+ subscribe() {
278
+ if (this._isSubscribed)
279
+ return;
280
+ this._isSubscribed = true;
281
+ this.client._subscribeToSession(this.sessionId).catch((error) => {
282
+ this._isSubscribed = false;
283
+ console.error(`Failed to subscribe to session ${this.sessionId}:`, error);
284
+ });
285
+ }
286
+ unsubscribe() {
287
+ if (!this._isSubscribed)
288
+ return;
289
+ this._isSubscribed = false;
290
+ this.client._unsubscribeFromSession(this.sessionId).catch((error) => {
291
+ console.error(`Failed to unsubscribe from session ${this.sessionId}:`, error);
292
+ });
293
+ }
294
+ send(input) {
295
+ return this.client.send(input, { sessionId: this.sessionId });
296
+ }
297
+ async abort(reason) {
298
+ await this.client.abort(this.sessionId, reason);
299
+ }
300
+ async close() {
301
+ await this.client.closeSession(this.sessionId);
302
+ }
303
+ submitToolResult(toolUseId, result) {
304
+ void this.client.submitToolResult(this.sessionId, toolUseId, result).catch(() => { });
305
+ }
306
+ onEvent(handler) {
307
+ this.eventHandlers.add(handler);
308
+ return () => {
309
+ this.eventHandlers.delete(handler);
310
+ };
311
+ }
312
+ onResult(handler) {
313
+ this.resultHandlers.add(handler);
314
+ return () => {
315
+ this.resultHandlers.delete(handler);
316
+ };
317
+ }
318
+ onToolConfirmation(handler) {
319
+ this.toolConfirmationHandlers.add(handler);
320
+ return () => {
321
+ this.toolConfirmationHandlers.delete(handler);
322
+ };
323
+ }
324
+ /** @internal - Called by client when event is received for this session */
325
+ _handleEvent(event) {
326
+ for (const handler of this.eventHandlers) {
327
+ try {
328
+ handler(event);
329
+ }
330
+ catch (error) {
331
+ console.error("Error in session event handler:", error);
332
+ }
333
+ }
334
+ }
335
+ /** @internal - Called by client when result is received for this session */
336
+ _handleResult(result) {
337
+ for (const handler of this.resultHandlers) {
338
+ try {
339
+ handler(result);
340
+ }
341
+ catch (error) {
342
+ console.error("Error in session result handler:", error);
343
+ }
344
+ }
345
+ }
346
+ /** @internal - Called by client when channel event is received for this session */
347
+ _handleChannelEvent(channelName, event) {
348
+ const channelAccessor = this.channels.get(channelName);
349
+ if (channelAccessor) {
350
+ channelAccessor._handleEvent(event);
351
+ }
352
+ }
353
+ /** @internal - Called by client when tool confirmation is requested */
354
+ _handleToolConfirmation(request) {
355
+ const respond = (response) => {
356
+ this.submitToolResult(request.toolUseId, response);
357
+ };
358
+ for (const handler of this.toolConfirmationHandlers) {
359
+ try {
360
+ handler(request, respond);
361
+ }
362
+ catch (error) {
363
+ console.error("Error in tool confirmation handler:", error);
364
+ }
365
+ }
366
+ }
367
+ channel(name) {
368
+ let channelAccessor = this.channels.get(name);
369
+ if (!channelAccessor) {
370
+ channelAccessor = new ChannelAccessorImpl(name, async (event) => {
371
+ await this.client._publishToChannel(this.sessionId, name, event);
372
+ });
373
+ this.channels.set(name, channelAccessor);
374
+ // Subscribe to this channel on the server
375
+ this.client._subscribeToChannel(this.sessionId, name).catch((err) => {
376
+ console.error(`Failed to subscribe to channel ${name}:`, err);
377
+ });
378
+ }
379
+ return channelAccessor;
380
+ }
381
+ /**
382
+ * Invoke a custom method with auto-injected sessionId.
383
+ *
384
+ * @example
385
+ * ```typescript
386
+ * const session = client.session("main");
387
+ * const tasks = await session.invoke("tasks:list");
388
+ * const newTask = await session.invoke("tasks:create", { title: "Buy groceries" });
389
+ * ```
390
+ */
391
+ async invoke(method, params = {}) {
392
+ return this.client.invoke(method, {
393
+ ...params,
394
+ sessionId: this.sessionId,
395
+ });
396
+ }
397
+ /**
398
+ * Invoke a streaming method with auto-injected sessionId.
399
+ *
400
+ * @example
401
+ * ```typescript
402
+ * const session = client.session("main");
403
+ * for await (const change of session.stream("tasks:watch")) {
404
+ * console.log("Task changed:", change);
405
+ * }
406
+ * ```
407
+ */
408
+ async *stream(method, params = {}) {
409
+ yield* this.client.stream(method, {
410
+ ...params,
411
+ sessionId: this.sessionId,
412
+ });
413
+ }
414
+ /** @internal */
415
+ _destroy() {
416
+ this.eventHandlers.clear();
417
+ this.resultHandlers.clear();
418
+ this.toolConfirmationHandlers.clear();
419
+ for (const channel of this.channels.values()) {
420
+ channel._destroy();
421
+ }
422
+ this.channels.clear();
423
+ }
424
+ }
425
+ // ============================================================================
426
+ // Utilities
427
+ // ============================================================================
428
+ function generateId() {
429
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
430
+ return crypto.randomUUID();
431
+ }
432
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
433
+ const r = (Math.random() * 16) | 0;
434
+ const v = c === "x" ? r : (r & 0x3) | 0x8;
435
+ return v.toString(16);
436
+ });
437
+ }
438
+ // ============================================================================
439
+ // AgentickClient
440
+ // ============================================================================
441
+ /**
442
+ * AgentickClient - Multiplexed session client.
443
+ *
444
+ * Connects to a Agentick server with a single SSE connection that
445
+ * can manage multiple session subscriptions.
446
+ *
447
+ * @example
448
+ * ```typescript
449
+ * const client = createClient({
450
+ * baseUrl: 'https://api.example.com',
451
+ * });
452
+ *
453
+ * // Get session accessor (cold - no subscription)
454
+ * const session = client.session('conv-123');
455
+ *
456
+ * // Subscribe to receive events (hot)
457
+ * session.subscribe();
458
+ *
459
+ * // Listen for events
460
+ * session.onEvent((event) => {
461
+ * console.log(event);
462
+ * });
463
+ *
464
+ * // Send a message
465
+ * const handle = session.send({ messages: [{ role: 'user', content: [...] }] });
466
+ * await handle.result;
467
+ *
468
+ * // Or use ephemeral send (creates session, executes, closes)
469
+ * const ephemeral = client.send({ messages: [{...}] });
470
+ * await ephemeral.result;
471
+ * ```
472
+ */
473
+ export class AgentickClient {
474
+ config;
475
+ fetchFn;
476
+ EventSourceCtor;
477
+ requestHeaders;
478
+ // Custom transport (when provided)
479
+ customTransport = null;
480
+ transportCleanup = null;
481
+ _state = "disconnected";
482
+ _connectionId;
483
+ eventSource;
484
+ connectionPromise;
485
+ stateHandlers = new Set();
486
+ eventHandlers = new Set();
487
+ streamingTextHandlers = new Set();
488
+ _streamingText = { text: "", isStreaming: false };
489
+ sessions = new Map();
490
+ subscriptions = new Set();
491
+ seenEventIds = new Set();
492
+ seenEventIdsOrder = [];
493
+ maxSeenEventIds = 5000;
494
+ constructor(config) {
495
+ this.config = config;
496
+ // Build request headers
497
+ this.requestHeaders = { "Content-Type": "application/json", ...config.headers };
498
+ if (config.token && !this.requestHeaders["Authorization"]) {
499
+ this.requestHeaders["Authorization"] = `Bearer ${config.token}`;
500
+ }
501
+ // Use custom implementations or fall back to globals
502
+ this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
503
+ this.EventSourceCtor = config.EventSource ?? globalThis.EventSource;
504
+ // Check if a custom transport was provided
505
+ if (config.transport && typeof config.transport === "object") {
506
+ this.customTransport = config.transport;
507
+ this.setupTransportHandlers();
508
+ }
509
+ }
510
+ /**
511
+ * Setup handlers for custom transport events.
512
+ */
513
+ setupTransportHandlers() {
514
+ if (!this.customTransport)
515
+ return;
516
+ // Forward transport events to our handlers
517
+ const cleanupEvent = this.customTransport.onEvent((event) => {
518
+ this.handleIncomingEvent(event);
519
+ });
520
+ // Map transport state to connection state
521
+ const cleanupState = this.customTransport.onStateChange((state) => {
522
+ const connectionState = state === "disconnected"
523
+ ? "disconnected"
524
+ : state === "connecting"
525
+ ? "connecting"
526
+ : state === "connected"
527
+ ? "connected"
528
+ : "error";
529
+ this.setState(connectionState);
530
+ });
531
+ this.transportCleanup = () => {
532
+ cleanupEvent();
533
+ cleanupState();
534
+ };
535
+ }
536
+ // ══════════════════════════════════════════════════════════════════════════
537
+ // Connection State
538
+ // ══════════════════════════════════════════════════════════════════════════
539
+ /** Current connection state */
540
+ get state() {
541
+ return this._state;
542
+ }
543
+ setState(state) {
544
+ if (this._state === state)
545
+ return;
546
+ this._state = state;
547
+ for (const handler of this.stateHandlers) {
548
+ try {
549
+ handler(state);
550
+ }
551
+ catch (error) {
552
+ console.error("Error in state handler:", error);
553
+ }
554
+ }
555
+ }
556
+ /**
557
+ * Subscribe to connection state changes.
558
+ */
559
+ onConnectionChange(handler) {
560
+ this.stateHandlers.add(handler);
561
+ return () => {
562
+ this.stateHandlers.delete(handler);
563
+ };
564
+ }
565
+ // ══════════════════════════════════════════════════════════════════════════
566
+ // Connection Lifecycle
567
+ // ══════════════════════════════════════════════════════════════════════════
568
+ /**
569
+ * Ensure the connection is established.
570
+ * This is called lazily when subscribing to sessions.
571
+ */
572
+ async ensureConnection() {
573
+ // Use custom transport if provided
574
+ if (this.customTransport) {
575
+ if (this.customTransport.state === "connected") {
576
+ this._connectionId = this.customTransport.connectionId;
577
+ return;
578
+ }
579
+ await this.customTransport.connect();
580
+ this._connectionId = this.customTransport.connectionId;
581
+ return;
582
+ }
583
+ // Default SSE connection logic
584
+ if (this._state === "connected") {
585
+ return;
586
+ }
587
+ if (this.connectionPromise) {
588
+ await this.connectionPromise;
589
+ return;
590
+ }
591
+ this.setState("connecting");
592
+ this.connectionPromise = this.openEventSource();
593
+ try {
594
+ await this.connectionPromise;
595
+ this.setState("connected");
596
+ }
597
+ catch (error) {
598
+ this.setState("error");
599
+ throw error;
600
+ }
601
+ finally {
602
+ this.connectionPromise = undefined;
603
+ }
604
+ }
605
+ async openEventSource() {
606
+ this.closeEventSource();
607
+ const baseUrl = this.config.baseUrl.replace(/\/$/, "");
608
+ const eventsPath = this.config.paths?.events ?? "/events";
609
+ const url = `${baseUrl}${eventsPath}`;
610
+ return new Promise((resolve, reject) => {
611
+ try {
612
+ this.eventSource = new this.EventSourceCtor(url, {
613
+ withCredentials: this.config.withCredentials,
614
+ });
615
+ const onMessage = (event) => {
616
+ try {
617
+ const data = JSON.parse(event.data);
618
+ this.handleIncomingEvent(data);
619
+ if (data.type === "connection" && data.connectionId) {
620
+ this._connectionId = data.connectionId;
621
+ if (data.subscriptions) {
622
+ for (const sessionId of data.subscriptions) {
623
+ this.subscriptions.add(sessionId);
624
+ }
625
+ }
626
+ resolve();
627
+ }
628
+ }
629
+ catch (error) {
630
+ console.error("Failed to parse SSE event:", error);
631
+ }
632
+ };
633
+ const onError = () => {
634
+ if (this._state === "connecting") {
635
+ this.closeEventSource();
636
+ reject(new Error("SSE connection failed"));
637
+ }
638
+ else {
639
+ this.setState("error");
640
+ }
641
+ };
642
+ this.eventSource.addEventListener("message", onMessage);
643
+ this.eventSource.addEventListener("error", onError);
644
+ }
645
+ catch (error) {
646
+ reject(error);
647
+ }
648
+ });
649
+ }
650
+ closeEventSource() {
651
+ if (!this.eventSource)
652
+ return;
653
+ this.eventSource.close();
654
+ this.eventSource = undefined;
655
+ this._connectionId = undefined;
656
+ this.subscriptions.clear();
657
+ this.setState("disconnected");
658
+ }
659
+ // ══════════════════════════════════════════════════════════════════════════
660
+ // Session Management
661
+ // ══════════════════════════════════════════════════════════════════════════
662
+ /**
663
+ * Get a session accessor (cold - no subscription).
664
+ *
665
+ * Call `accessor.subscribe()` to receive events.
666
+ */
667
+ session(sessionId) {
668
+ let accessor = this.sessions.get(sessionId);
669
+ if (!accessor) {
670
+ accessor = new SessionAccessorImpl(sessionId, this);
671
+ this.sessions.set(sessionId, accessor);
672
+ }
673
+ return accessor;
674
+ }
675
+ /**
676
+ * Subscribe to a session and get accessor (hot).
677
+ */
678
+ subscribe(sessionId) {
679
+ const accessor = this.session(sessionId);
680
+ accessor.subscribe();
681
+ return accessor;
682
+ }
683
+ /** @internal - Called by SessionAccessor */
684
+ async _subscribeToSession(sessionId) {
685
+ await this.ensureConnection();
686
+ if (this.subscriptions.has(sessionId)) {
687
+ return;
688
+ }
689
+ // Use custom transport if provided
690
+ if (this.customTransport) {
691
+ await this.customTransport.subscribeToSession(sessionId);
692
+ this.subscriptions.add(sessionId);
693
+ return;
694
+ }
695
+ if (!this._connectionId) {
696
+ throw new Error("Connection not established");
697
+ }
698
+ const baseUrl = this.config.baseUrl.replace(/\/$/, "");
699
+ const subscribePath = this.config.paths?.subscribe ?? "/subscribe";
700
+ const response = await this.fetchFn(`${baseUrl}${subscribePath}`, {
701
+ method: "POST",
702
+ headers: this.requestHeaders,
703
+ credentials: this.config.withCredentials ? "include" : "same-origin",
704
+ body: JSON.stringify({
705
+ connectionId: this._connectionId,
706
+ add: [sessionId],
707
+ }),
708
+ signal: AbortSignal.timeout(this.config.timeout ?? 30000),
709
+ });
710
+ if (!response.ok) {
711
+ const text = await response.text();
712
+ throw new Error(`Failed to subscribe: ${response.status} ${text}`);
713
+ }
714
+ this.subscriptions.add(sessionId);
715
+ }
716
+ /** @internal - Called by SessionAccessor */
717
+ async _unsubscribeFromSession(sessionId) {
718
+ if (!this.subscriptions.has(sessionId)) {
719
+ return;
720
+ }
721
+ // Use custom transport if provided
722
+ if (this.customTransport) {
723
+ await this.customTransport.unsubscribeFromSession(sessionId);
724
+ this.subscriptions.delete(sessionId);
725
+ return;
726
+ }
727
+ if (!this._connectionId) {
728
+ return;
729
+ }
730
+ const baseUrl = this.config.baseUrl.replace(/\/$/, "");
731
+ const subscribePath = this.config.paths?.subscribe ?? "/subscribe";
732
+ await this.fetchFn(`${baseUrl}${subscribePath}`, {
733
+ method: "POST",
734
+ headers: this.requestHeaders,
735
+ credentials: this.config.withCredentials ? "include" : "same-origin",
736
+ body: JSON.stringify({
737
+ connectionId: this._connectionId,
738
+ remove: [sessionId],
739
+ }),
740
+ signal: AbortSignal.timeout(this.config.timeout ?? 30000),
741
+ });
742
+ this.subscriptions.delete(sessionId);
743
+ }
744
+ /** @internal - Called by SessionAccessor to publish to a channel */
745
+ async _publishToChannel(sessionId, channelName, event) {
746
+ // Use custom transport if provided
747
+ if (this.customTransport) {
748
+ await this.customTransport.publishToChannel(sessionId, channelName, event);
749
+ return;
750
+ }
751
+ const baseUrl = this.config.baseUrl.replace(/\/$/, "");
752
+ const channelPath = this.config.paths?.channel ?? "/channel";
753
+ const response = await this.fetchFn(`${baseUrl}${channelPath}`, {
754
+ method: "POST",
755
+ headers: this.requestHeaders,
756
+ credentials: this.config.withCredentials ? "include" : "same-origin",
757
+ body: JSON.stringify({
758
+ sessionId,
759
+ channel: channelName,
760
+ type: event.type,
761
+ payload: event.payload,
762
+ id: event.id,
763
+ metadata: event.metadata,
764
+ }),
765
+ signal: AbortSignal.timeout(this.config.timeout ?? 30000),
766
+ });
767
+ if (!response.ok) {
768
+ const text = await response.text();
769
+ throw new Error(`Failed to publish to channel: ${response.status} ${text}`);
770
+ }
771
+ }
772
+ /** @internal - Called by SessionAccessor to subscribe to a channel */
773
+ async _subscribeToChannel(sessionId, channelName) {
774
+ // Ensure we have a connection before subscribing
775
+ await this.ensureConnection();
776
+ // Use custom transport if provided
777
+ if (this.customTransport) {
778
+ await this.customTransport.subscribeToChannel(sessionId, channelName);
779
+ return;
780
+ }
781
+ const baseUrl = this.config.baseUrl.replace(/\/$/, "");
782
+ const channelPath = this.config.paths?.channel ?? "/channel";
783
+ const response = await this.fetchFn(`${baseUrl}${channelPath}/subscribe`, {
784
+ method: "POST",
785
+ headers: this.requestHeaders,
786
+ credentials: this.config.withCredentials ? "include" : "same-origin",
787
+ body: JSON.stringify({
788
+ sessionId,
789
+ channel: channelName,
790
+ clientId: this._connectionId,
791
+ }),
792
+ signal: AbortSignal.timeout(this.config.timeout ?? 30000),
793
+ });
794
+ if (!response.ok) {
795
+ const text = await response.text();
796
+ throw new Error(`Failed to subscribe to channel: ${response.status} ${text}`);
797
+ }
798
+ }
799
+ // ══════════════════════════════════════════════════════════════════════════
800
+ // Message Operations
801
+ // ══════════════════════════════════════════════════════════════════════════
802
+ /**
803
+ * Send a message.
804
+ *
805
+ * @param input - Message input (message or messages)
806
+ * @param options - Options including optional sessionId
807
+ */
808
+ send(input, options) {
809
+ const payload = this.normalizeSendInput(input);
810
+ const abortController = new AbortController();
811
+ const handle = new ClientExecutionHandleImpl(this, abortController, options?.sessionId);
812
+ void this.performSend(payload, options, handle, abortController);
813
+ return handle;
814
+ }
815
+ async performSend(payload, options, handle, abortController) {
816
+ try {
817
+ // Use custom transport if provided
818
+ if (this.customTransport) {
819
+ const stream = this.customTransport.send(payload, options?.sessionId);
820
+ // Wire up abort
821
+ abortController.signal.addEventListener("abort", () => {
822
+ stream.abort(abortController.signal.reason ?? "Aborted");
823
+ });
824
+ for await (const event of stream) {
825
+ if (event.type === "channel" || event.type === "connection") {
826
+ continue;
827
+ }
828
+ handle._handleEvent(event);
829
+ this.handleIncomingEvent(event);
830
+ }
831
+ handle._complete();
832
+ return;
833
+ }
834
+ // Default HTTP fetch logic
835
+ const baseUrl = this.config.baseUrl.replace(/\/$/, "");
836
+ const sendPath = this.config.paths?.send ?? "/send";
837
+ const body = { ...payload };
838
+ if (options?.sessionId) {
839
+ body.sessionId = options.sessionId;
840
+ }
841
+ const response = await this.fetchFn(`${baseUrl}${sendPath}`, {
842
+ method: "POST",
843
+ headers: this.requestHeaders,
844
+ credentials: this.config.withCredentials ? "include" : "same-origin",
845
+ body: JSON.stringify(body),
846
+ signal: abortController.signal,
847
+ });
848
+ if (!response.ok) {
849
+ const text = await response.text();
850
+ throw new Error(`Failed to send: ${response.status} ${text}`);
851
+ }
852
+ if (!response.body) {
853
+ throw new Error("No response body for send");
854
+ }
855
+ const reader = response.body.getReader();
856
+ const decoder = new TextDecoder();
857
+ let buffer = "";
858
+ while (true) {
859
+ const { done, value } = await reader.read();
860
+ if (done)
861
+ break;
862
+ buffer += decoder.decode(value, { stream: true });
863
+ const lines = buffer.split("\n");
864
+ buffer = lines.pop() ?? "";
865
+ for (const line of lines) {
866
+ if (!line.startsWith("data: "))
867
+ continue;
868
+ try {
869
+ const data = JSON.parse(line.slice(6));
870
+ if (data.type === "channel" || data.type === "connection") {
871
+ continue;
872
+ }
873
+ const event = data;
874
+ handle._handleEvent(event);
875
+ this.handleIncomingEvent(data);
876
+ }
877
+ catch (error) {
878
+ console.error("Failed to parse send event:", error);
879
+ }
880
+ }
881
+ }
882
+ handle._complete();
883
+ }
884
+ catch (error) {
885
+ handle._fail(error instanceof Error ? error : new Error(String(error)));
886
+ }
887
+ }
888
+ normalizeSendInput(input) {
889
+ if (typeof input === "string") {
890
+ return { messages: [{ role: "user", content: [{ type: "text", text: input }] }] };
891
+ }
892
+ if (Array.isArray(input)) {
893
+ if (input.length === 0) {
894
+ return { messages: [] };
895
+ }
896
+ if (typeof input[0].role === "string") {
897
+ return { messages: input };
898
+ }
899
+ return { messages: [{ role: "user", content: input }] };
900
+ }
901
+ if (typeof input === "object" && input && "role" in input && "content" in input) {
902
+ return { messages: [input] };
903
+ }
904
+ return input;
905
+ }
906
+ /**
907
+ * Abort a session's current execution.
908
+ */
909
+ async abort(sessionId, reason) {
910
+ // Use custom transport if provided
911
+ if (this.customTransport) {
912
+ await this.customTransport.abortSession(sessionId, reason);
913
+ return;
914
+ }
915
+ const baseUrl = this.config.baseUrl.replace(/\/$/, "");
916
+ const abortPath = this.config.paths?.abort ?? "/abort";
917
+ const response = await this.fetchFn(`${baseUrl}${abortPath}`, {
918
+ method: "POST",
919
+ headers: this.requestHeaders,
920
+ credentials: this.config.withCredentials ? "include" : "same-origin",
921
+ body: JSON.stringify({ sessionId, reason }),
922
+ signal: AbortSignal.timeout(this.config.timeout ?? 30000),
923
+ });
924
+ if (!response.ok) {
925
+ const text = await response.text();
926
+ throw new Error(`Failed to abort: ${response.status} ${text}`);
927
+ }
928
+ }
929
+ /**
930
+ * Close a session.
931
+ */
932
+ async closeSession(sessionId) {
933
+ // Use custom transport if provided
934
+ if (this.customTransport) {
935
+ await this.customTransport.closeSession(sessionId);
936
+ }
937
+ else {
938
+ const baseUrl = this.config.baseUrl.replace(/\/$/, "");
939
+ const closePath = this.config.paths?.close ?? "/close";
940
+ const response = await this.fetchFn(`${baseUrl}${closePath}`, {
941
+ method: "POST",
942
+ headers: this.requestHeaders,
943
+ credentials: this.config.withCredentials ? "include" : "same-origin",
944
+ body: JSON.stringify({ sessionId }),
945
+ signal: AbortSignal.timeout(this.config.timeout ?? 30000),
946
+ });
947
+ if (!response.ok) {
948
+ const text = await response.text();
949
+ throw new Error(`Failed to close: ${response.status} ${text}`);
950
+ }
951
+ }
952
+ // Clean up accessor
953
+ const accessor = this.sessions.get(sessionId);
954
+ if (accessor) {
955
+ accessor._destroy();
956
+ this.sessions.delete(sessionId);
957
+ }
958
+ this.subscriptions.delete(sessionId);
959
+ }
960
+ /**
961
+ * Submit a tool confirmation response.
962
+ */
963
+ async submitToolResult(sessionId, toolUseId, result) {
964
+ // Use custom transport if provided
965
+ if (this.customTransport) {
966
+ await this.customTransport.submitToolResult(sessionId, toolUseId, result);
967
+ return;
968
+ }
969
+ const baseUrl = this.config.baseUrl.replace(/\/$/, "");
970
+ const toolResponsePath = this.config.paths?.toolResponse ?? "/tool-response";
971
+ const response = await this.fetchFn(`${baseUrl}${toolResponsePath}`, {
972
+ method: "POST",
973
+ headers: this.requestHeaders,
974
+ credentials: this.config.withCredentials ? "include" : "same-origin",
975
+ body: JSON.stringify({ sessionId, toolUseId, result }),
976
+ signal: AbortSignal.timeout(this.config.timeout ?? 30000),
977
+ });
978
+ if (!response.ok) {
979
+ const text = await response.text();
980
+ throw new Error(`Failed to submit tool result: ${response.status} ${text}`);
981
+ }
982
+ }
983
+ // ══════════════════════════════════════════════════════════════════════════
984
+ // Event Handling
985
+ // ══════════════════════════════════════════════════════════════════════════
986
+ /**
987
+ * Subscribe to all stream events (from all sessions).
988
+ */
989
+ onEvent(handler) {
990
+ this.eventHandlers.add(handler);
991
+ return () => {
992
+ this.eventHandlers.delete(handler);
993
+ };
994
+ }
995
+ /**
996
+ * Ergonomic event subscription.
997
+ */
998
+ on(eventName, handler) {
999
+ switch (eventName) {
1000
+ case "event":
1001
+ return this.onEvent(handler);
1002
+ case "state":
1003
+ return this.onConnectionChange(handler);
1004
+ default: {
1005
+ const streamType = eventName;
1006
+ const streamHandler = handler;
1007
+ return this.onEvent((event) => {
1008
+ if (event.type === streamType) {
1009
+ streamHandler(event);
1010
+ }
1011
+ });
1012
+ }
1013
+ }
1014
+ }
1015
+ handleIncomingEvent(data) {
1016
+ const sessionId = data.sessionId;
1017
+ const type = data.type;
1018
+ // Handle connection event
1019
+ if (type === "connection") {
1020
+ return;
1021
+ }
1022
+ // Handle channel events (from server → client)
1023
+ if (type === "channel" && sessionId) {
1024
+ const channelName = data.channel;
1025
+ const channelEvent = data.event;
1026
+ if (channelName && channelEvent) {
1027
+ const accessor = this.sessions.get(sessionId);
1028
+ if (accessor) {
1029
+ accessor._handleChannelEvent(channelName, channelEvent);
1030
+ }
1031
+ }
1032
+ return;
1033
+ }
1034
+ // Route stream events
1035
+ const streamEvent = data;
1036
+ const eventId = streamEvent.id;
1037
+ if (eventId) {
1038
+ if (this.seenEventIds.has(eventId)) {
1039
+ return;
1040
+ }
1041
+ this.seenEventIds.add(eventId);
1042
+ this.seenEventIdsOrder.push(eventId);
1043
+ if (this.seenEventIdsOrder.length > this.maxSeenEventIds) {
1044
+ const oldest = this.seenEventIdsOrder.shift();
1045
+ if (oldest) {
1046
+ this.seenEventIds.delete(oldest);
1047
+ }
1048
+ }
1049
+ }
1050
+ // Update streaming text state
1051
+ this.updateStreamingText(streamEvent);
1052
+ // Notify global handlers
1053
+ if (sessionId) {
1054
+ const sessionEvent = streamEvent;
1055
+ for (const handler of this.eventHandlers) {
1056
+ try {
1057
+ handler(sessionEvent);
1058
+ }
1059
+ catch (error) {
1060
+ console.error("Error in event handler:", error);
1061
+ }
1062
+ }
1063
+ }
1064
+ // Notify session-specific handlers
1065
+ if (sessionId) {
1066
+ const accessor = this.sessions.get(sessionId);
1067
+ if (accessor) {
1068
+ accessor._handleEvent(streamEvent);
1069
+ }
1070
+ }
1071
+ // Handle result events
1072
+ if (type === "result" && "result" in data) {
1073
+ if (sessionId) {
1074
+ const accessor = this.sessions.get(sessionId);
1075
+ if (accessor) {
1076
+ accessor._handleResult(data.result);
1077
+ }
1078
+ }
1079
+ }
1080
+ // Handle tool confirmation requests
1081
+ if (type === "tool_confirmation_required" && sessionId) {
1082
+ const required = data;
1083
+ const request = {
1084
+ toolUseId: required.callId,
1085
+ name: required.name,
1086
+ arguments: required.input,
1087
+ message: required.message,
1088
+ };
1089
+ const accessor = this.sessions.get(sessionId);
1090
+ if (accessor) {
1091
+ accessor._handleToolConfirmation(request);
1092
+ }
1093
+ }
1094
+ }
1095
+ // ══════════════════════════════════════════════════════════════════════════
1096
+ // Streaming Text
1097
+ // ══════════════════════════════════════════════════════════════════════════
1098
+ /** Current streaming text state */
1099
+ get streamingText() {
1100
+ return this._streamingText;
1101
+ }
1102
+ /**
1103
+ * Subscribe to streaming text state changes.
1104
+ */
1105
+ onStreamingText(handler) {
1106
+ this.streamingTextHandlers.add(handler);
1107
+ handler(this._streamingText);
1108
+ return () => {
1109
+ this.streamingTextHandlers.delete(handler);
1110
+ };
1111
+ }
1112
+ /** Clear the accumulated streaming text */
1113
+ clearStreamingText() {
1114
+ this.setStreamingText({ text: "", isStreaming: false });
1115
+ }
1116
+ setStreamingText(state) {
1117
+ this._streamingText = state;
1118
+ for (const handler of this.streamingTextHandlers) {
1119
+ try {
1120
+ handler(state);
1121
+ }
1122
+ catch (error) {
1123
+ console.error("Error in streaming text handler:", error);
1124
+ }
1125
+ }
1126
+ }
1127
+ updateStreamingText(event) {
1128
+ switch (event.type) {
1129
+ case "tick_start":
1130
+ this.setStreamingText({ text: "", isStreaming: true });
1131
+ break;
1132
+ case "content_delta":
1133
+ this.setStreamingText({
1134
+ text: this._streamingText.text + event.delta,
1135
+ isStreaming: true,
1136
+ });
1137
+ break;
1138
+ case "tick_end":
1139
+ case "execution_end":
1140
+ this.setStreamingText({
1141
+ text: this._streamingText.text,
1142
+ isStreaming: false,
1143
+ });
1144
+ break;
1145
+ }
1146
+ }
1147
+ // ══════════════════════════════════════════════════════════════════════════
1148
+ // Custom Method Invocation
1149
+ // ══════════════════════════════════════════════════════════════════════════
1150
+ /**
1151
+ * Invoke a custom Gateway method.
1152
+ * For session-scoped methods, use session.invoke() instead.
1153
+ *
1154
+ * @example
1155
+ * ```typescript
1156
+ * // Invoke a custom method
1157
+ * const result = await client.invoke("tasks:list", { status: "active" });
1158
+ *
1159
+ * // Invoke with admin method
1160
+ * const stats = await client.invoke("admin:stats");
1161
+ * ```
1162
+ */
1163
+ async invoke(method, params = {}) {
1164
+ const baseUrl = this.config.baseUrl.replace(/\/$/, "");
1165
+ const invokePath = this.config.paths?.invoke ?? "/invoke";
1166
+ const response = await this.fetchFn(`${baseUrl}${invokePath}`, {
1167
+ method: "POST",
1168
+ headers: this.requestHeaders,
1169
+ credentials: this.config.withCredentials ? "include" : "same-origin",
1170
+ body: JSON.stringify({
1171
+ method,
1172
+ params,
1173
+ }),
1174
+ signal: AbortSignal.timeout(this.config.timeout ?? 30000),
1175
+ });
1176
+ if (!response.ok) {
1177
+ const text = await response.text();
1178
+ throw new Error(`Failed to invoke method: ${response.status} ${text}`);
1179
+ }
1180
+ const result = await response.json();
1181
+ return result;
1182
+ }
1183
+ /**
1184
+ * Invoke a streaming method, returns async iterator.
1185
+ * Yields values as they arrive from the server.
1186
+ *
1187
+ * @example
1188
+ * ```typescript
1189
+ * // Stream task updates
1190
+ * for await (const change of client.stream("tasks:watch")) {
1191
+ * console.log("Task changed:", change);
1192
+ * }
1193
+ * ```
1194
+ */
1195
+ async *stream(method, params = {}) {
1196
+ const baseUrl = this.config.baseUrl.replace(/\/$/, "");
1197
+ const invokePath = this.config.paths?.invoke ?? "/invoke";
1198
+ const response = await this.fetchFn(`${baseUrl}${invokePath}`, {
1199
+ method: "POST",
1200
+ headers: this.requestHeaders,
1201
+ credentials: this.config.withCredentials ? "include" : "same-origin",
1202
+ body: JSON.stringify({
1203
+ method,
1204
+ params,
1205
+ }),
1206
+ });
1207
+ if (!response.ok) {
1208
+ const text = await response.text();
1209
+ throw new Error(`Failed to invoke streaming method: ${response.status} ${text}`);
1210
+ }
1211
+ if (!response.body) {
1212
+ throw new Error("No response body for streaming method");
1213
+ }
1214
+ const reader = response.body.getReader();
1215
+ const decoder = new TextDecoder();
1216
+ let buffer = "";
1217
+ while (true) {
1218
+ const { done, value } = await reader.read();
1219
+ if (done)
1220
+ break;
1221
+ buffer += decoder.decode(value, { stream: true });
1222
+ const lines = buffer.split("\n");
1223
+ buffer = lines.pop() ?? "";
1224
+ for (const line of lines) {
1225
+ if (!line.startsWith("data: "))
1226
+ continue;
1227
+ try {
1228
+ const data = JSON.parse(line.slice(6));
1229
+ if (data.type === "method:chunk") {
1230
+ yield data.chunk;
1231
+ }
1232
+ else if (data.type === "method:end") {
1233
+ return;
1234
+ }
1235
+ }
1236
+ catch (error) {
1237
+ console.error("Failed to parse stream event:", error);
1238
+ }
1239
+ }
1240
+ }
1241
+ }
1242
+ /**
1243
+ * Get authorization headers for use with fetch.
1244
+ * Useful for making authenticated requests to custom routes.
1245
+ *
1246
+ * @example
1247
+ * ```typescript
1248
+ * // Make authenticated request to custom API
1249
+ * const response = await fetch("/api/custom", {
1250
+ * headers: client.getAuthHeaders(),
1251
+ * });
1252
+ * ```
1253
+ */
1254
+ getAuthHeaders() {
1255
+ const headers = {};
1256
+ if (this.config.token) {
1257
+ headers["Authorization"] = `Bearer ${this.config.token}`;
1258
+ }
1259
+ return headers;
1260
+ }
1261
+ // ══════════════════════════════════════════════════════════════════════════
1262
+ // Cleanup
1263
+ // ══════════════════════════════════════════════════════════════════════════
1264
+ /**
1265
+ * Cleanup and close the client.
1266
+ */
1267
+ destroy() {
1268
+ // Clean up custom transport
1269
+ if (this.customTransport) {
1270
+ this.transportCleanup?.();
1271
+ this.customTransport.disconnect();
1272
+ }
1273
+ else {
1274
+ this.closeEventSource();
1275
+ }
1276
+ for (const accessor of this.sessions.values()) {
1277
+ accessor._destroy();
1278
+ }
1279
+ this.sessions.clear();
1280
+ this.stateHandlers.clear();
1281
+ this.eventHandlers.clear();
1282
+ this.streamingTextHandlers.clear();
1283
+ this._streamingText = { text: "", isStreaming: false };
1284
+ this.seenEventIds.clear();
1285
+ this.seenEventIdsOrder = [];
1286
+ this.subscriptions.clear();
1287
+ }
1288
+ /**
1289
+ * Get the underlying transport (if using custom transport).
1290
+ * Useful for accessing transport-specific features like leadership status.
1291
+ */
1292
+ getTransport() {
1293
+ return this.customTransport ?? undefined;
1294
+ }
1295
+ }
1296
+ // ============================================================================
1297
+ // Transport Detection
1298
+ // ============================================================================
1299
+ /**
1300
+ * Detect the appropriate transport based on URL scheme.
1301
+ */
1302
+ function detectTransport(baseUrl) {
1303
+ const url = baseUrl.toLowerCase();
1304
+ if (url.startsWith("ws://") || url.startsWith("wss://")) {
1305
+ return "websocket";
1306
+ }
1307
+ return "sse";
1308
+ }
1309
+ // ============================================================================
1310
+ // Factory Function
1311
+ // ============================================================================
1312
+ /**
1313
+ * Create a new AgentickClient.
1314
+ *
1315
+ * Transport is auto-detected from the URL scheme:
1316
+ * - http:// or https:// -> SSE transport
1317
+ * - ws:// or wss:// -> WebSocket transport
1318
+ *
1319
+ * You can also explicitly set the transport in the config, or provide
1320
+ * a custom ClientTransport instance (e.g., SharedTransport for multi-tab).
1321
+ *
1322
+ * @example
1323
+ * ```typescript
1324
+ * // Auto-detect transport (SSE for http://)
1325
+ * const client = createClient({
1326
+ * baseUrl: 'https://api.example.com',
1327
+ * });
1328
+ *
1329
+ * // Auto-detect transport (WebSocket for ws://)
1330
+ * const wsClient = createClient({
1331
+ * baseUrl: 'ws://localhost:18789',
1332
+ * });
1333
+ *
1334
+ * // Force WebSocket transport
1335
+ * const wsClient2 = createClient({
1336
+ * baseUrl: 'http://localhost:3000',
1337
+ * transport: 'websocket',
1338
+ * });
1339
+ *
1340
+ * // Custom transport (e.g., SharedTransport for multi-tab)
1341
+ * import { createSharedTransport } from '@agentick/client-multiplexer';
1342
+ * const sharedClient = createClient({
1343
+ * baseUrl: 'https://api.example.com',
1344
+ * transport: createSharedTransport({ baseUrl: 'https://api.example.com' }),
1345
+ * });
1346
+ *
1347
+ * // Subscribe to a session
1348
+ * const session = client.subscribe('conv-123');
1349
+ *
1350
+ * // Send a message
1351
+ * const handle = session.send({ messages: [{ role: 'user', content: [...] }] });
1352
+ * await handle.result;
1353
+ * ```
1354
+ */
1355
+ export function createClient(config) {
1356
+ // If a custom transport object is provided, use it directly
1357
+ if (config.transport && typeof config.transport === "object") {
1358
+ return new AgentickClient(config);
1359
+ }
1360
+ const transport = config.transport === "auto" || !config.transport
1361
+ ? detectTransport(config.baseUrl)
1362
+ : config.transport;
1363
+ if (transport === "websocket") {
1364
+ // Log warning - WebSocket transport requires using the WSTransport directly
1365
+ // or the gateway client for full functionality
1366
+ console.warn("[AgentickClient] WebSocket URL detected. For full WebSocket support, " +
1367
+ "use createWSTransport() directly or connect to a Gateway. " +
1368
+ "Falling back to SSE transport with URL conversion.");
1369
+ // Convert ws:// to http:// for SSE fallback
1370
+ let baseUrl = config.baseUrl;
1371
+ if (baseUrl.startsWith("ws://")) {
1372
+ baseUrl = baseUrl.replace("ws://", "http://");
1373
+ }
1374
+ else if (baseUrl.startsWith("wss://")) {
1375
+ baseUrl = baseUrl.replace("wss://", "https://");
1376
+ }
1377
+ return new AgentickClient({ ...config, baseUrl });
1378
+ }
1379
+ return new AgentickClient(config);
1380
+ }
1381
+ //# sourceMappingURL=client.js.map