@symbolica/agentica 0.3.2 → 0.4.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/LICENSE +21 -77
- package/README.md +7 -10
- package/dist/bin/agentica-setup.js +0 -0
- package/dist/src/agentica/agent.d.ts +3 -1
- package/dist/src/agentica/agent.d.ts.map +1 -1
- package/dist/src/agentica/agent.js +4 -5
- package/dist/src/agentica/agent.js.map +1 -1
- package/dist/src/agentica/agentic.d.ts +4 -1
- package/dist/src/agentica/agentic.d.ts.map +1 -1
- package/dist/src/agentica/agentic.js +4 -4
- package/dist/src/agentica/agentic.js.map +1 -1
- package/dist/src/agentica/common.d.ts +10 -6
- package/dist/src/agentica/common.d.ts.map +1 -1
- package/dist/src/agentica/common.js +33 -19
- package/dist/src/agentica/common.js.map +1 -1
- package/dist/src/client-session-manager/client-session-manager.d.ts +11 -9
- package/dist/src/client-session-manager/client-session-manager.d.ts.map +1 -1
- package/dist/src/client-session-manager/client-session-manager.js +186 -101
- package/dist/src/client-session-manager/client-session-manager.js.map +1 -1
- package/dist/src/client-session-manager/types.d.ts +1 -1
- package/dist/src/client-session-manager/types.d.ts.map +1 -1
- package/dist/src/coming-soon.d.ts +7 -18
- package/dist/src/coming-soon.d.ts.map +1 -1
- package/dist/src/coming-soon.js +21 -41
- package/dist/src/coming-soon.js.map +1 -1
- package/dist/src/errors/base.d.ts.map +1 -1
- package/dist/src/errors/base.js +3 -1
- package/dist/src/errors/base.js.map +1 -1
- package/dist/src/errors/bugs.js +1 -1
- package/dist/src/errors/bugs.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.d.ts.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/src/warpc/msg-protocol/concept/python-mirrors/str.d.ts +7 -0
- package/dist/src/warpc/msg-protocol/concept/python-mirrors/str.d.ts.map +1 -0
- package/dist/src/warpc/msg-protocol/concept/python-mirrors/str.js +144 -0
- package/dist/src/warpc/msg-protocol/concept/python-mirrors/str.js.map +1 -0
- package/dist/src/warpc/msg-protocol/concept/python-mirrors/str.test.d.ts +2 -0
- package/dist/src/warpc/msg-protocol/concept/python-mirrors/str.test.d.ts.map +1 -0
- package/dist/src/warpc/msg-protocol/concept/python-mirrors/str.test.js +253 -0
- package/dist/src/warpc/msg-protocol/concept/python-mirrors/str.test.js.map +1 -0
- package/dist/src/warpc/msg-protocol/concept/python-mirrors/utils.d.ts +5 -0
- package/dist/src/warpc/msg-protocol/concept/python-mirrors/utils.d.ts.map +1 -0
- package/dist/src/warpc/msg-protocol/concept/python-mirrors/utils.js +17 -0
- package/dist/src/warpc/msg-protocol/concept/python-mirrors/utils.js.map +1 -0
- package/dist/src/warpc/msg-protocol/concept/resource/system-msg.d.ts +2 -1
- package/dist/src/warpc/msg-protocol/concept/resource/system-msg.d.ts.map +1 -1
- package/dist/src/warpc/msg-protocol/concept/resource/system-msg.js +14 -10
- package/dist/src/warpc/msg-protocol/concept/resource/system-msg.js.map +1 -1
- package/dist-transformer/agentica/agent.d.ts +6 -1
- package/dist-transformer/agentica/agent.d.ts.map +1 -1
- package/dist-transformer/agentica/agent.js +4 -4
- package/dist-transformer/agentica/agentic.d.ts +6 -1
- package/dist-transformer/agentica/agentic.d.ts.map +1 -1
- package/dist-transformer/agentica/agentic.js +4 -4
- package/dist-transformer/agentica/common.d.ts +23 -14
- package/dist-transformer/agentica/common.d.ts.map +1 -1
- package/dist-transformer/agentica/common.js +47 -27
- package/dist-transformer/client-session-manager/client-session-manager.d.ts +21 -13
- package/dist-transformer/client-session-manager/client-session-manager.d.ts.map +1 -1
- package/dist-transformer/client-session-manager/client-session-manager.js +233 -115
- package/dist-transformer/client-session-manager/types.d.ts +1 -1
- package/dist-transformer/client-session-manager/types.d.ts.map +1 -1
- package/dist-transformer/coming-soon.d.ts +7 -18
- package/dist-transformer/coming-soon.d.ts.map +1 -1
- package/dist-transformer/coming-soon.js +21 -39
- package/dist-transformer/errors/base.d.ts.map +1 -1
- package/dist-transformer/errors/base.js +3 -1
- package/dist-transformer/errors/bugs.js +1 -1
- package/dist-transformer/version.d.ts +1 -1
- package/dist-transformer/version.d.ts.map +1 -1
- package/dist-transformer/version.js +1 -1
- package/dist-transformer/warpc/msg-protocol/concept/python-mirrors/str.d.ts +7 -0
- package/dist-transformer/warpc/msg-protocol/concept/python-mirrors/str.d.ts.map +1 -0
- package/dist-transformer/warpc/msg-protocol/concept/python-mirrors/str.js +151 -0
- package/dist-transformer/warpc/msg-protocol/concept/python-mirrors/utils.d.ts +5 -0
- package/dist-transformer/warpc/msg-protocol/concept/python-mirrors/utils.d.ts.map +1 -0
- package/dist-transformer/warpc/msg-protocol/concept/python-mirrors/utils.js +16 -0
- package/dist-transformer/warpc/msg-protocol/concept/resource/system-msg.d.ts +2 -1
- package/dist-transformer/warpc/msg-protocol/concept/resource/system-msg.d.ts.map +1 -1
- package/dist-transformer/warpc/msg-protocol/concept/resource/system-msg.js +14 -11
- package/package.json +143 -141
|
@@ -35,9 +35,10 @@ import { version } from '../version.js';
|
|
|
35
35
|
*/
|
|
36
36
|
export class ClientSessionManager {
|
|
37
37
|
constructor(config = {}) {
|
|
38
|
-
|
|
39
|
-
this.
|
|
40
|
-
this.
|
|
38
|
+
// Per-session-manager WebSocket state (sm_id → connection state)
|
|
39
|
+
this.sessionManagers = new Map();
|
|
40
|
+
this.smLocks = new Map();
|
|
41
|
+
this.uidToSm = new Map(); // agent uid → sm_id
|
|
41
42
|
this.baseHttp = null;
|
|
42
43
|
this.baseWs = null;
|
|
43
44
|
this.apiKey = null;
|
|
@@ -158,31 +159,30 @@ export class ClientSessionManager {
|
|
|
158
159
|
return headers;
|
|
159
160
|
}
|
|
160
161
|
/**
|
|
161
|
-
* Background task for writing messages to WebSocket
|
|
162
|
+
* Background task for writing messages to WebSocket for a specific session manager.
|
|
162
163
|
*/
|
|
163
|
-
async
|
|
164
|
+
async smWriter(smId, control) {
|
|
164
165
|
await waitForTracing;
|
|
165
|
-
const wsLogger = this.logger.withScope(
|
|
166
|
+
const wsLogger = this.logger.withScope(`ws-writer-${smId.slice(0, 8)}`);
|
|
166
167
|
wsLogger.debug('Writer task started');
|
|
167
|
-
const ws = this.websocket;
|
|
168
168
|
try {
|
|
169
169
|
while (!control.shouldStop) {
|
|
170
|
+
const smc = this.sessionManagers.get(smId);
|
|
171
|
+
if (!smc) {
|
|
172
|
+
wsLogger.debug('Session manager connection removed, exiting writer');
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
170
175
|
try {
|
|
171
|
-
const
|
|
172
|
-
if (!sendQueue) {
|
|
173
|
-
wsLogger.debug('Send queue removed, exiting writer');
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
const msg = await sendQueue.get();
|
|
176
|
+
const msg = await smc.sendQueue.get();
|
|
177
177
|
const msgUid = msg.uid;
|
|
178
178
|
const msgLogger = msgUid ? (this.uidToLogger.get(msgUid) ?? wsLogger) : wsLogger;
|
|
179
|
-
if (
|
|
179
|
+
if (smc.websocket.readyState !== 1) {
|
|
180
180
|
const error = new WebSocketConnectionError(`cannot send as websocket is not open.`);
|
|
181
181
|
enrichError(error, { uid: msgUid, sessionId: this.clientSessionId });
|
|
182
182
|
throw error;
|
|
183
183
|
}
|
|
184
184
|
msgLogger.debug(`Sending ${msg.type} message`);
|
|
185
|
-
|
|
185
|
+
smc.websocket.send(encodeMessage(msg));
|
|
186
186
|
}
|
|
187
187
|
catch (error) {
|
|
188
188
|
if (error instanceof Error && error.message === 'Queue closed') {
|
|
@@ -196,26 +196,24 @@ export class ClientSessionManager {
|
|
|
196
196
|
}
|
|
197
197
|
catch (error) {
|
|
198
198
|
wsLogger.error('Writer task failed', error);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
this.matchIid.delete(matchId);
|
|
202
|
-
}
|
|
203
|
-
for (const [_, exceptionMap] of this.uidIidException.entries()) {
|
|
204
|
-
for (const [_iid, handlers] of exceptionMap.entries()) {
|
|
205
|
-
handlers.reject(new WebSocketConnectionError(`WebSocket writer failed: ${error.message}`));
|
|
206
|
-
}
|
|
207
|
-
}
|
|
199
|
+
// Clean up this session manager only - don't affect others
|
|
200
|
+
await this._cleanupSessionManager(smId);
|
|
208
201
|
throw error;
|
|
209
202
|
}
|
|
210
203
|
}
|
|
211
204
|
/**
|
|
212
|
-
* Background task for reading messages from WebSocket
|
|
205
|
+
* Background task for reading messages from WebSocket for a specific session manager.
|
|
213
206
|
*/
|
|
214
|
-
async
|
|
207
|
+
async smReader(smId, control) {
|
|
215
208
|
await waitForTracing;
|
|
216
|
-
const wsLogger = this.logger.withScope(
|
|
209
|
+
const wsLogger = this.logger.withScope(`ws-reader-${smId.slice(0, 8)}`);
|
|
217
210
|
wsLogger.debug('Reader task started');
|
|
218
|
-
const
|
|
211
|
+
const smc = this.sessionManagers.get(smId);
|
|
212
|
+
if (!smc) {
|
|
213
|
+
wsLogger.warn('Session manager connection not found');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const ws = smc.websocket;
|
|
219
217
|
ws.onmessage = (event) => {
|
|
220
218
|
if (control.shouldStop)
|
|
221
219
|
return;
|
|
@@ -317,48 +315,125 @@ export class ClientSessionManager {
|
|
|
317
315
|
ws.onclose = (event) => {
|
|
318
316
|
wsLogger.debug(`WebSocket closed (code=${event.code}, reason=${event.reason || 'none'})`);
|
|
319
317
|
control.shouldStop = true;
|
|
320
|
-
for
|
|
321
|
-
|
|
322
|
-
this.matchIid.delete(matchId);
|
|
323
|
-
}
|
|
324
|
-
for (const [_, exceptionMap] of this.uidIidException.entries()) {
|
|
325
|
-
for (const [_iid, handlers] of exceptionMap.entries()) {
|
|
326
|
-
handlers.reject(new WebSocketConnectionError(`WebSocket closed: ${event.reason || 'Connection closed'}`));
|
|
327
|
-
}
|
|
328
|
-
exceptionMap.clear();
|
|
329
|
-
}
|
|
318
|
+
// Only fail futures for agents on THIS session manager
|
|
319
|
+
this._failSmFutures(smId, `WebSocket closed: ${event.reason || 'Connection closed'}`);
|
|
330
320
|
resolve();
|
|
331
321
|
};
|
|
332
322
|
ws.onerror = (_error) => {
|
|
333
323
|
wsLogger.error('WebSocket error');
|
|
334
324
|
control.shouldStop = true;
|
|
335
|
-
for
|
|
336
|
-
|
|
337
|
-
this.matchIid.delete(matchId);
|
|
338
|
-
}
|
|
339
|
-
for (const [_, exceptionMap] of this.uidIidException.entries()) {
|
|
340
|
-
for (const [_iid, handlers] of exceptionMap.entries()) {
|
|
341
|
-
handlers.reject(new WebSocketConnectionError('WebSocket error occurred'));
|
|
342
|
-
}
|
|
343
|
-
exceptionMap.clear();
|
|
344
|
-
}
|
|
325
|
+
// Only fail futures for agents on THIS session manager
|
|
326
|
+
this._failSmFutures(smId, 'WebSocket error occurred');
|
|
345
327
|
resolve();
|
|
346
328
|
};
|
|
347
329
|
});
|
|
348
330
|
}
|
|
349
331
|
/**
|
|
350
|
-
*
|
|
332
|
+
* Fail pending futures for agents on a specific session manager.
|
|
333
|
+
*/
|
|
334
|
+
_failSmFutures(smId, reason) {
|
|
335
|
+
// Get all UIDs that belong to this session manager
|
|
336
|
+
const affectedUids = [];
|
|
337
|
+
for (const [uid, sid] of this.uidToSm.entries()) {
|
|
338
|
+
if (sid === smId) {
|
|
339
|
+
affectedUids.push(uid);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Fail matchIid promises for affected UIDs (we don't track which session manager a match_id belongs to,
|
|
343
|
+
// so we fail all of them when any session manager fails - this is conservative but safe)
|
|
344
|
+
for (const [matchId, handlers] of this.matchIid.entries()) {
|
|
345
|
+
handlers.reject(new WebSocketConnectionError(reason));
|
|
346
|
+
this.matchIid.delete(matchId);
|
|
347
|
+
}
|
|
348
|
+
// Fail exception handlers for affected UIDs
|
|
349
|
+
for (const uid of affectedUids) {
|
|
350
|
+
const exceptionMap = this.uidIidException.get(uid);
|
|
351
|
+
if (exceptionMap) {
|
|
352
|
+
for (const [_iid, handlers] of exceptionMap.entries()) {
|
|
353
|
+
handlers.reject(new WebSocketConnectionError(reason));
|
|
354
|
+
}
|
|
355
|
+
exceptionMap.clear();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Clean up a specific session manager's connection without affecting others.
|
|
361
|
+
*/
|
|
362
|
+
async _cleanupSessionManager(smId) {
|
|
363
|
+
const smc = this.sessionManagers.get(smId);
|
|
364
|
+
if (!smc) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
this.logger.debug(`Cleaning up session manager ${smId.slice(0, 8)}`);
|
|
368
|
+
this.sessionManagers.delete(smId);
|
|
369
|
+
this.smLocks.delete(smId);
|
|
370
|
+
// Stop tasks
|
|
371
|
+
const [readerControl, writerControl] = smc.tasks;
|
|
372
|
+
readerControl.shouldStop = true;
|
|
373
|
+
writerControl.shouldStop = true;
|
|
374
|
+
// Close send queue
|
|
375
|
+
smc.sendQueue.close();
|
|
376
|
+
// Close WebSocket gracefully
|
|
377
|
+
try {
|
|
378
|
+
if (smc.websocket.readyState === 0 || smc.websocket.readyState === 1) {
|
|
379
|
+
smc.websocket.close(1000, 'Normal closure');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
this.logger.warn(`Failed to close WebSocket for session manager ${smId.slice(0, 8)}`, error);
|
|
384
|
+
}
|
|
385
|
+
// Wait for tasks to complete
|
|
386
|
+
await Promise.allSettled([readerControl.promise, writerControl.promise]);
|
|
387
|
+
// Fail pending futures for agents on this session manager
|
|
388
|
+
this._failSmFutures(smId, 'Session manager connection closed');
|
|
389
|
+
// Clean up agent-to-session-manager mappings
|
|
390
|
+
const affectedUids = [];
|
|
391
|
+
for (const [uid, sid] of this.uidToSm.entries()) {
|
|
392
|
+
if (sid === smId) {
|
|
393
|
+
affectedUids.push(uid);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
for (const uid of affectedUids) {
|
|
397
|
+
this.uidToSm.delete(uid);
|
|
398
|
+
}
|
|
399
|
+
this.logger.debug(`Cleaned up session manager ${smId.slice(0, 8)}`);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Ensure WebSocket connection exists for a specific session manager, creating it if needed.
|
|
351
403
|
*/
|
|
352
|
-
async
|
|
353
|
-
|
|
404
|
+
async _ensureSmConnection(smId) {
|
|
405
|
+
// Fast path: connection already exists
|
|
406
|
+
const existing = this.sessionManagers.get(smId);
|
|
407
|
+
if (existing && existing.websocket.readyState === 1) {
|
|
354
408
|
return;
|
|
355
409
|
}
|
|
410
|
+
// Check if another call is already creating this connection
|
|
411
|
+
const existingLock = this.smLocks.get(smId);
|
|
412
|
+
if (existingLock) {
|
|
413
|
+
await existingLock;
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
// Create lock IMMEDIATELY after check - no await between check and set
|
|
417
|
+
let resolveLock;
|
|
418
|
+
const lockPromise = new Promise((resolve) => {
|
|
419
|
+
resolveLock = resolve;
|
|
420
|
+
});
|
|
421
|
+
this.smLocks.set(smId, lockPromise);
|
|
422
|
+
// Now safe to yield
|
|
356
423
|
await waitForTracing;
|
|
357
|
-
const span = this.logger.startSpan('sdk.
|
|
424
|
+
const span = this.logger.startSpan('sdk.sm_connection');
|
|
425
|
+
span.setAttribute('session_manager.id', smId);
|
|
358
426
|
try {
|
|
359
|
-
|
|
427
|
+
// Double-check after acquiring lock
|
|
428
|
+
const doubleCheck = this.sessionManagers.get(smId);
|
|
429
|
+
if (doubleCheck && doubleCheck.websocket.readyState === 1) {
|
|
430
|
+
resolveLock();
|
|
431
|
+
this.smLocks.delete(smId);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const sendQueue = new Queue();
|
|
360
435
|
const websocket_uri = `${this.baseWs}/socket`;
|
|
361
|
-
this.logger.debug(`Connecting WebSocket`);
|
|
436
|
+
this.logger.debug(`Connecting WebSocket for session manager ${smId.slice(0, 8)}`);
|
|
362
437
|
span.setAttribute('websocket.uri', websocket_uri);
|
|
363
438
|
span.setAttribute('websocket.state', 'connecting');
|
|
364
439
|
// Create headers object with trace context for distributed tracing
|
|
@@ -368,22 +443,18 @@ export class ClientSessionManager {
|
|
|
368
443
|
}
|
|
369
444
|
headers['x-client-session-id'] = this.clientSessionId;
|
|
370
445
|
// Inject trace context into WebSocket headers for distributed tracing
|
|
371
|
-
// Execute in span's context to ensure proper trace propagation (like Python's inject(headers, context=ctx))
|
|
372
446
|
if (span.executeInContext) {
|
|
373
447
|
span.executeInContext(() => {
|
|
374
448
|
injectTraceContext(headers);
|
|
375
449
|
});
|
|
376
450
|
}
|
|
377
451
|
else {
|
|
378
|
-
// Fallback: pass span context directly
|
|
379
452
|
injectTraceContext(headers, span);
|
|
380
453
|
}
|
|
381
|
-
// Pass null for apiKey since headers already has Authorization
|
|
382
454
|
const ws = await createWebSocket(websocket_uri, this.apiKey, headers);
|
|
383
455
|
ws.binaryType = 'arraybuffer';
|
|
384
456
|
return new Promise((resolve, reject) => {
|
|
385
457
|
ws.onopen = () => {
|
|
386
|
-
this.websocket = ws;
|
|
387
458
|
try {
|
|
388
459
|
ws._socket?.unref?.();
|
|
389
460
|
}
|
|
@@ -398,23 +469,34 @@ export class ClientSessionManager {
|
|
|
398
469
|
shouldStop: false,
|
|
399
470
|
promise: Promise.resolve(),
|
|
400
471
|
};
|
|
401
|
-
|
|
402
|
-
|
|
472
|
+
// Store the session manager connection first so tasks can access it
|
|
473
|
+
const smc = {
|
|
474
|
+
websocket: ws,
|
|
475
|
+
sendQueue,
|
|
476
|
+
tasks: [readerControl, writerControl],
|
|
477
|
+
};
|
|
478
|
+
this.sessionManagers.set(smId, smc);
|
|
479
|
+
// Start per-session-manager reader and writer tasks
|
|
480
|
+
readerControl.promise = this.smReader(smId, readerControl).catch((error) => {
|
|
481
|
+
this.logger.error(`Reader task crashed for session manager ${smId.slice(0, 8)}`, error);
|
|
403
482
|
});
|
|
404
|
-
writerControl.promise = this.
|
|
405
|
-
this.logger.error(
|
|
483
|
+
writerControl.promise = this.smWriter(smId, writerControl).catch((error) => {
|
|
484
|
+
this.logger.error(`Writer task crashed for session manager ${smId.slice(0, 8)}`, error);
|
|
406
485
|
});
|
|
407
|
-
this.
|
|
408
|
-
this.logger.info(`WebSocket connected`);
|
|
486
|
+
this.logger.info(`WebSocket connected for session manager ${smId.slice(0, 8)}`);
|
|
409
487
|
span.setAttribute('websocket.state', 'connected');
|
|
410
488
|
span.end();
|
|
489
|
+
resolveLock();
|
|
490
|
+
this.smLocks.delete(smId);
|
|
411
491
|
resolve();
|
|
412
492
|
};
|
|
413
493
|
ws.onerror = (error) => {
|
|
414
|
-
this.logger.error(`WebSocket connection failed:\n${error.toString()}`);
|
|
494
|
+
this.logger.error(`WebSocket connection failed for session manager ${smId.slice(0, 8)}:\n${error.toString()}`);
|
|
415
495
|
span.setAttribute('websocket.state', 'error');
|
|
416
496
|
span.recordException(error);
|
|
417
497
|
span.end();
|
|
498
|
+
resolveLock();
|
|
499
|
+
this.smLocks.delete(smId);
|
|
418
500
|
reject(new WebSocketConnectionError('WebSocket connection failed'));
|
|
419
501
|
};
|
|
420
502
|
});
|
|
@@ -423,6 +505,8 @@ export class ClientSessionManager {
|
|
|
423
505
|
span.setAttribute('websocket.state', 'error');
|
|
424
506
|
span.recordException(error);
|
|
425
507
|
span.end();
|
|
508
|
+
resolveLock();
|
|
509
|
+
this.smLocks.delete(smId);
|
|
426
510
|
throw error;
|
|
427
511
|
}
|
|
428
512
|
}
|
|
@@ -503,10 +587,26 @@ export class ClientSessionManager {
|
|
|
503
587
|
logger.warn(message);
|
|
504
588
|
}
|
|
505
589
|
}
|
|
506
|
-
|
|
590
|
+
// Parse JSON response: { uid, session_manager_id }
|
|
591
|
+
const responseData = await response.json();
|
|
592
|
+
const uid = responseData.uid;
|
|
593
|
+
const smId = responseData.session_manager_id;
|
|
594
|
+
// Validate response fields
|
|
595
|
+
if (!uid) {
|
|
596
|
+
logger.error(`Server response missing "uid" field: ${JSON.stringify(responseData)}`);
|
|
597
|
+
throw new ConnectionError('Invalid server response: missing "uid" field');
|
|
598
|
+
}
|
|
599
|
+
if (!smId) {
|
|
600
|
+
logger.error(`Server response missing "session_manager_id" field: ${JSON.stringify(responseData)}`);
|
|
601
|
+
throw new ConnectionError('Invalid server response: missing "session_manager_id" field');
|
|
602
|
+
}
|
|
507
603
|
logger.info('Created agent');
|
|
508
604
|
span.setAttribute('agent.uid', uid);
|
|
509
|
-
|
|
605
|
+
span.setAttribute('agent.session_manager_id', smId);
|
|
606
|
+
// Track which session manager this agent belongs to
|
|
607
|
+
this.uidToSm.set(uid, smId);
|
|
608
|
+
// Ensure WebSocket connection for this session manager
|
|
609
|
+
await this._ensureSmConnection(smId);
|
|
510
610
|
this.knownUids.add(uid);
|
|
511
611
|
this._initAgentState(uid, parentLogger);
|
|
512
612
|
return uid;
|
|
@@ -542,8 +642,22 @@ export class ClientSessionManager {
|
|
|
542
642
|
enrichError(error, { uid, sessionId: this.clientSessionId });
|
|
543
643
|
throw error;
|
|
544
644
|
}
|
|
645
|
+
// Get session manager for this agent
|
|
646
|
+
const smId = this.uidToSm.get(uid);
|
|
647
|
+
if (!smId) {
|
|
648
|
+
const error = new WebSocketConnectionError(`No session manager found for agent ${uid}`);
|
|
649
|
+
enrichError(error, { uid, sessionId: this.clientSessionId });
|
|
650
|
+
throw error;
|
|
651
|
+
}
|
|
652
|
+
const smc = this.sessionManagers.get(smId);
|
|
653
|
+
if (!smc || smc.websocket.readyState !== 1) {
|
|
654
|
+
const error = new WebSocketConnectionError(`No active connection for session manager ${smId}`);
|
|
655
|
+
enrichError(error, { uid, sessionId: this.clientSessionId });
|
|
656
|
+
throw error;
|
|
657
|
+
}
|
|
545
658
|
// Add attributes matching Python SDK
|
|
546
659
|
span.setAttribute('agent.uid', uid);
|
|
660
|
+
span.setAttribute('agent.session_manager_id', smId);
|
|
547
661
|
span.setAttribute('agent.task_desc', typeof taskDesc === 'string' ? taskDesc : taskDesc.template);
|
|
548
662
|
span.setAttribute('invocation.streaming', streaming);
|
|
549
663
|
const matchId = this.idIssuer();
|
|
@@ -556,11 +670,12 @@ export class ClientSessionManager {
|
|
|
556
670
|
prompt: taskDesc,
|
|
557
671
|
streaming: streaming,
|
|
558
672
|
};
|
|
559
|
-
invLogger.debug(`Invoking with match_id=${matchId}`);
|
|
673
|
+
invLogger.debug(`Invoking with match_id=${matchId} on session manager ${smId.slice(0, 8)}`);
|
|
560
674
|
const iidPromise = new Promise((resolve, reject) => {
|
|
561
675
|
this.matchIid.set(matchId, { resolve, reject });
|
|
562
676
|
});
|
|
563
|
-
|
|
677
|
+
// Route to the correct session manager's send queue
|
|
678
|
+
smc.sendQueue.put(msg);
|
|
564
679
|
const iid = await iidPromise;
|
|
565
680
|
this.iidToUid.set(iid, uid);
|
|
566
681
|
invLogger.info(`Invocation created with iid=${iid}`);
|
|
@@ -577,8 +692,14 @@ export class ClientSessionManager {
|
|
|
577
692
|
return { resolvers: { resolve, reject, promise }, promise };
|
|
578
693
|
})();
|
|
579
694
|
this.uidIidException.get(uid).set(iid, resolvers);
|
|
695
|
+
// Capture smId for the closure
|
|
696
|
+
const capturedSmId = smId;
|
|
580
697
|
return {
|
|
581
698
|
send_message: async (data) => {
|
|
699
|
+
const currentSmc = this.sessionManagers.get(capturedSmId);
|
|
700
|
+
if (!currentSmc || currentSmc.websocket.readyState !== 1) {
|
|
701
|
+
throw new WebSocketConnectionError(`Session manager connection closed for ${capturedSmId}`);
|
|
702
|
+
}
|
|
582
703
|
const dataMsg = {
|
|
583
704
|
type: 'data',
|
|
584
705
|
uid,
|
|
@@ -586,7 +707,7 @@ export class ClientSessionManager {
|
|
|
586
707
|
data,
|
|
587
708
|
timestamp: new Date().toISOString(),
|
|
588
709
|
};
|
|
589
|
-
|
|
710
|
+
currentSmc.sendQueue.put(dataMsg);
|
|
590
711
|
invLogger.debug(`Queued ${data.length} bytes to send`);
|
|
591
712
|
},
|
|
592
713
|
recv_message: async () => {
|
|
@@ -610,7 +731,7 @@ export class ClientSessionManager {
|
|
|
610
731
|
/**
|
|
611
732
|
* Echo streaming endpoint
|
|
612
733
|
*/
|
|
613
|
-
async *echo(cancelSignal, uid, iid) {
|
|
734
|
+
async *echo(cancelSignal, uid, iid, includeUsage = false) {
|
|
614
735
|
if (!this.baseHttp) {
|
|
615
736
|
const error = new ConnectionError('Base HTTP endpoint must be set');
|
|
616
737
|
enrichError(error, { uid, iid, sessionId: this.clientSessionId });
|
|
@@ -670,8 +791,16 @@ export class ClientSessionManager {
|
|
|
670
791
|
const logmsg = JSON.parse(line);
|
|
671
792
|
const typ = logmsg.type;
|
|
672
793
|
if (typ === 'sm_invocation_exit') {
|
|
673
|
-
|
|
674
|
-
|
|
794
|
+
// Only return if listening to a specific invocation.
|
|
795
|
+
// When iid is undefined, we're listening to all invocations
|
|
796
|
+
// for this agent and should continue across invocations.
|
|
797
|
+
if (iid !== undefined) {
|
|
798
|
+
this.logger.debug('Received invocation exit signal, ending stream');
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
yield { role: 'agent', content: '', type: 'invocation_exit' };
|
|
802
|
+
isStreaming = false;
|
|
803
|
+
continue;
|
|
675
804
|
}
|
|
676
805
|
if (typ === 'sm_monad' && logmsg.body) {
|
|
677
806
|
const body = JSON.parse(logmsg.body);
|
|
@@ -681,7 +810,7 @@ export class ClientSessionManager {
|
|
|
681
810
|
isStreaming = true;
|
|
682
811
|
const delta = body.args[0];
|
|
683
812
|
if (delta && delta.content) {
|
|
684
|
-
yield { role: 'agent', content: delta.content };
|
|
813
|
+
yield { role: 'agent', content: delta.content, type: delta.type };
|
|
685
814
|
chunkCount++;
|
|
686
815
|
}
|
|
687
816
|
}
|
|
@@ -693,11 +822,15 @@ export class ClientSessionManager {
|
|
|
693
822
|
// Skip system messages
|
|
694
823
|
continue;
|
|
695
824
|
}
|
|
696
|
-
else if (isStreaming && role === 'agent') {
|
|
697
|
-
// Skip agent messages as they're already handled in stream_chunk
|
|
825
|
+
else if (isStreaming && role === 'agent' && delta.type !== 'usage') {
|
|
826
|
+
// Skip agent messages as they're already handled in stream_chunk,
|
|
827
|
+
// except usage chunks which are only sent via delta
|
|
698
828
|
continue;
|
|
699
829
|
}
|
|
700
|
-
|
|
830
|
+
if (delta.type === 'usage' && !includeUsage) {
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
yield { role, content: delta.content, type: delta.type };
|
|
701
834
|
chunkCount++;
|
|
702
835
|
}
|
|
703
836
|
}
|
|
@@ -804,29 +937,34 @@ export class ClientSessionManager {
|
|
|
804
937
|
* @param lastTotal - Previous cumulative total (for cross-invocation adjustment)
|
|
805
938
|
* @returns Object containing the usage for this invocation and the new cumulative total
|
|
806
939
|
*/
|
|
807
|
-
async fetchUsage(uid, iid,
|
|
940
|
+
async fetchUsage(uid, iid, _lastTotal) {
|
|
808
941
|
// Fetch logs for this invocation and sum usages
|
|
942
|
+
// Each log entry contains non-cumulative usage values, so we just add them up
|
|
943
|
+
// The logs are already filtered by iid, so they only contain this invocation's data
|
|
809
944
|
let total = Usage.zero();
|
|
810
945
|
const logs = await this.logs(uid, iid, { type: 'sm_inference_usage' });
|
|
811
946
|
for (const log of logs) {
|
|
812
947
|
const logUsage = log.usage;
|
|
813
948
|
if (logUsage) {
|
|
814
|
-
|
|
949
|
+
// Don't pass lastUsage - each log entry is already non-cumulative
|
|
950
|
+
total = total.add(Usage.fromCompletions(logUsage));
|
|
815
951
|
}
|
|
816
952
|
}
|
|
817
|
-
|
|
818
|
-
if (lastTotal) {
|
|
819
|
-
// Subtract previous cumulative total, but keep output_tokens as-is
|
|
820
|
-
// since output_tokens is already absolute (not cumulative)
|
|
821
|
-
usage = total.sub(lastTotal.replace({ outputTokens: 0 }));
|
|
822
|
-
}
|
|
823
|
-
return { usage, newTotal: total };
|
|
953
|
+
return { usage: total, newTotal: total };
|
|
824
954
|
}
|
|
825
955
|
/**
|
|
826
956
|
* Check if agent exists - matches Python agent_exists
|
|
827
957
|
*/
|
|
828
958
|
agentExists(uid) {
|
|
829
|
-
|
|
959
|
+
if (!this.knownUids.has(uid)) {
|
|
960
|
+
return false;
|
|
961
|
+
}
|
|
962
|
+
const smId = this.uidToSm.get(uid);
|
|
963
|
+
if (!smId) {
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
const smc = this.sessionManagers.get(smId);
|
|
967
|
+
return smc !== undefined && smc.websocket.readyState === 1; // 1 = OPEN
|
|
830
968
|
}
|
|
831
969
|
/**
|
|
832
970
|
* Get uid from iid (used for parent-child agent tracking)
|
|
@@ -867,7 +1005,7 @@ export class ClientSessionManager {
|
|
|
867
1005
|
}
|
|
868
1006
|
}
|
|
869
1007
|
/**
|
|
870
|
-
* Close a specific agent's resources (does not close the
|
|
1008
|
+
* Close a specific agent's resources (does not close the session manager's WebSocket unless it's the last agent)
|
|
871
1009
|
*/
|
|
872
1010
|
async closeAgent(uid) {
|
|
873
1011
|
const logger = this.uidToLogger.get(uid) ?? this.logger;
|
|
@@ -890,6 +1028,7 @@ export class ClientSessionManager {
|
|
|
890
1028
|
this.uidIidException.delete(uid);
|
|
891
1029
|
this.knownUids.delete(uid);
|
|
892
1030
|
this.uidToLogger.delete(uid);
|
|
1031
|
+
this.uidToSm.delete(uid);
|
|
893
1032
|
logger.debug(`Closed agent ${uid.slice(0, 8)}`);
|
|
894
1033
|
// Finally, destroy the agent on the server
|
|
895
1034
|
await this.destroyAgent(uid);
|
|
@@ -905,37 +1044,16 @@ export class ClientSessionManager {
|
|
|
905
1044
|
const span = this.logger.startSpan('stop');
|
|
906
1045
|
try {
|
|
907
1046
|
const agentCount = this.knownUids.size;
|
|
908
|
-
this.
|
|
1047
|
+
const smCount = this.sessionManagers.size;
|
|
1048
|
+
this.logger.debug(`CSM Stop: stopping with ${agentCount} agent(s) on ${smCount} session manager(s)`);
|
|
909
1049
|
span.setAttribute('agent_count', agentCount);
|
|
1050
|
+
span.setAttribute('session_manager_count', smCount);
|
|
910
1051
|
// Close all agents (cleans up per-agent state and destroys on server)
|
|
911
1052
|
const uids = Array.from(this.knownUids);
|
|
912
1053
|
await Promise.allSettled(uids.map((uid) => this.closeAgent(uid)));
|
|
913
|
-
//
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
this.sendQueue = null;
|
|
917
|
-
}
|
|
918
|
-
// Close WebSocket gracefully
|
|
919
|
-
if (this.websocket) {
|
|
920
|
-
try {
|
|
921
|
-
// 0 = CONNECTING, 1 = OPEN
|
|
922
|
-
if (this.websocket.readyState === 0 || this.websocket.readyState === 1) {
|
|
923
|
-
this.websocket.close(1000, 'Normal closure');
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
catch (error) {
|
|
927
|
-
this.logger.warn('Failed to close WebSocket', error);
|
|
928
|
-
}
|
|
929
|
-
this.websocket = null;
|
|
930
|
-
}
|
|
931
|
-
// Stop tasks
|
|
932
|
-
if (this.tasks) {
|
|
933
|
-
const [readerControl, writerControl] = this.tasks;
|
|
934
|
-
readerControl.shouldStop = true;
|
|
935
|
-
writerControl.shouldStop = true;
|
|
936
|
-
await Promise.allSettled([readerControl.promise, writerControl.promise]);
|
|
937
|
-
this.tasks = null;
|
|
938
|
-
}
|
|
1054
|
+
// Clean up all session manager connections
|
|
1055
|
+
const smIds = Array.from(this.sessionManagers.keys());
|
|
1056
|
+
await Promise.allSettled(smIds.map((smId) => this._cleanupSessionManager(smId)));
|
|
939
1057
|
this.logger.debug('All resources cleaned up');
|
|
940
1058
|
this.isStopped = true;
|
|
941
1059
|
// Ensure invocation guard is cleared
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { components, operations, paths } from '../types/api.d';
|
|
2
2
|
import type { components } from '../types/api.d';
|
|
3
|
-
export type CreateAgentRequest = Omit<components['schemas']['CreateAgentRequest'], 'warp_globals_payload' | 'protocol'> & {
|
|
3
|
+
export type CreateAgentRequest = Omit<components['schemas']['CreateAgentRequest'], 'warp_globals_payload' | 'protocol' | 'json'> & {
|
|
4
4
|
warp_globals_payload: Uint8Array;
|
|
5
5
|
protocol?: string;
|
|
6
6
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client-session-manager/types.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGpE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAIjD,MAAM,MAAM,kBAAkB,GAAG,IAAI,CACjC,UAAU,CAAC,SAAS,CAAC,CAAC,oBAAoB,CAAC,EAC3C,sBAAsB,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client-session-manager/types.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGpE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAIjD,MAAM,MAAM,kBAAkB,GAAG,IAAI,CACjC,UAAU,CAAC,SAAS,CAAC,CAAC,oBAAoB,CAAC,EAC3C,sBAAsB,GAAG,UAAU,GAAG,MAAM,CAC/C,GAAG;IACA,oBAAoB,EAAE,UAAU,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,gBAAgB,CAAC,CAAC;AAGrE,MAAM,MAAM,sBAAsB,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,wBAAwB,CAAC,CAAC;AACrF,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC,GAAG;IAC7F,IAAI,EAAE,UAAU,CAAC;CACpB,CAAC;AACF,MAAM,MAAM,qBAAqB,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,uBAAuB,CAAC,CAAC;AACnF,MAAM,MAAM,0BAA0B,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,iCAAiC,CAAC,CAAC;AAClG,MAAM,MAAM,sBAAsB,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,wBAAwB,CAAC,EAAE,qBAAqB,CAAC,GAAG;IAChH,mBAAmB,EAAE,UAAU,CAAC;CACnC,CAAC;AACF,MAAM,MAAM,uBAAuB,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,yBAAyB,CAAC,CAAC;AAGvF,MAAM,MAAM,gBAAgB,GACtB,oBAAoB,GACpB,qBAAqB,GACrB,0BAA0B,GAC1B,uBAAuB,GACvB,sBAAsB,GACtB,sBAAsB,CAAC;AAE7B,MAAM,MAAM,sBAAsB,GAAG,sBAAsB,GAAG,oBAAoB,GAAG,sBAAsB,CAAC;AAE5G,MAAM,MAAM,8BAA8B,GAAG,oBAAoB,GAAG,sBAAsB,CAAC;AAE3F,MAAM,MAAM,sBAAsB,GAC5B,uBAAuB,GACvB,qBAAqB,GACrB,oBAAoB,GACpB,0BAA0B,CAAC;AAEjC,MAAM,MAAM,8BAA8B,GAAG,qBAAqB,GAAG,oBAAoB,GAAG,0BAA0B,CAAC"}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { FrameContext } from './warpc/frame-context/frame-ctx.js';
|
|
2
|
-
import type { ToolModeStrings } from './agentica/common.js';
|
|
3
2
|
declare enum Feature {
|
|
4
|
-
JSON_MODE = "JSON mode",
|
|
5
3
|
ENUM_VALUES = "Enum support",
|
|
6
4
|
FUTURE_VALUES = "Promises and futures",
|
|
7
5
|
LOCAL_FILE_ACCESS = "Local file access",
|
|
@@ -9,11 +7,9 @@ declare enum Feature {
|
|
|
9
7
|
NUMERIC_RANGES = "Numeric range objects",
|
|
10
8
|
BINARY_BUFFERS = "Binary buffer objects",
|
|
11
9
|
URL_OBJECTS = "URL objects",
|
|
12
|
-
JSON_BASE_EXCEPTIONS = "Richer exceptions in JSON mode",
|
|
13
10
|
CUSTOM_EXCEPTIONS = "Custom exceptions",
|
|
14
11
|
TYPE = "Type",
|
|
15
12
|
TYPE_CONSTRUCTORS = "Type constructor",
|
|
16
|
-
ARBITRARY = "Arbitrary",
|
|
17
13
|
VIRTUALIZATION = "Virtualization",
|
|
18
14
|
UNSUPPORTED_BUILTIN_CLASS = "Unsupported builtin class",
|
|
19
15
|
GENERATORS = "Generators",
|
|
@@ -22,23 +18,16 @@ declare enum Feature {
|
|
|
22
18
|
}
|
|
23
19
|
export declare class ComingSoon extends Error {
|
|
24
20
|
feature: Feature;
|
|
25
|
-
mode?: ToolModeStrings;
|
|
26
21
|
detail?: string;
|
|
27
|
-
constructor(feature: Feature, mode?: ToolModeStrings, detail?: string);
|
|
28
|
-
}
|
|
29
|
-
export declare class TryCodeMode extends Error {
|
|
30
|
-
feature: Feature;
|
|
31
|
-
detail: string;
|
|
32
22
|
constructor(feature: Feature, detail?: string);
|
|
33
23
|
}
|
|
34
|
-
export declare const validateFeature: (thing: any
|
|
35
|
-
export declare const constructorTypes: (
|
|
36
|
-
export declare const unsupportedBultinClass: (className: string
|
|
37
|
-
export declare const unsupportedEnum: (enumName: string
|
|
38
|
-
export declare const interfaceReturnType: (
|
|
39
|
-
export declare const intersectionReturnType: (
|
|
40
|
-
export declare const validateReturnType: (returnType: any, context: FrameContext
|
|
41
|
-
export declare const throwNoJsonMode: () => never;
|
|
24
|
+
export declare const validateFeature: (thing: any) => void;
|
|
25
|
+
export declare const constructorTypes: () => ComingSoon;
|
|
26
|
+
export declare const unsupportedBultinClass: (className: string) => ComingSoon;
|
|
27
|
+
export declare const unsupportedEnum: (enumName: string) => ComingSoon;
|
|
28
|
+
export declare const interfaceReturnType: () => ComingSoon;
|
|
29
|
+
export declare const intersectionReturnType: () => ComingSoon;
|
|
30
|
+
export declare const validateReturnType: (returnType: any, context: FrameContext) => void;
|
|
42
31
|
export declare const throwNoVirtualization: () => never;
|
|
43
32
|
export declare const throwNoBareFutures: () => never;
|
|
44
33
|
export default ComingSoon;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coming-soon.d.ts","sourceRoot":"","sources":["../src/coming-soon.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"coming-soon.d.ts","sourceRoot":"","sources":["../src/coming-soon.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAIpE,aAAK,OAAO;IACR,WAAW,iBAAiB;IAC5B,aAAa,yBAAyB;IACtC,iBAAiB,sBAAsB;IACvC,kBAAkB,8BAA8B;IAChD,cAAc,0BAA0B;IACxC,cAAc,0BAA0B;IACxC,WAAW,gBAAgB;IAC3B,iBAAiB,sBAAsB;IACvC,IAAI,SAAS;IACb,iBAAiB,qBAAqB;IACtC,cAAc,mBAAmB;IACjC,yBAAyB,8BAA8B;IACvD,UAAU,eAAe;IACzB,qBAAqB,6BAA6B;IAClD,wBAAwB,gCAAgC;CAC3D;AAED,qBAAa,UAAW,SAAQ,KAAK;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;gBAEJ,OAAO,EAAE,OAAO,EAAE,MAAM,GAAE,MAAW;CAKpD;AAwDD,eAAO,MAAM,eAAe,GAAI,OAAO,GAAG,KAAG,IA8C5C,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAO,UAEnC,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAAI,WAAW,MAAM,KAAG,UAE1D,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,UAAU,MAAM,KAAG,UAKlD,CAAC;AAEF,eAAO,MAAM,mBAAmB,QAAO,UAEtC,CAAC;AAEF,eAAO,MAAM,sBAAsB,QAAO,UAEzC,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,YAAY,GAAG,EAAE,SAAS,YAAY,KAAG,IAqC3E,CAAC;AAEF,eAAO,MAAM,qBAAqB,aAEjC,CAAC;AAEF,eAAO,MAAM,kBAAkB,aAE9B,CAAC;AAEF,eAAe,UAAU,CAAC"}
|