@egain/ai-agent-sdk 0.1.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.
- package/README.md +295 -0
- package/dist/browser.js +22739 -0
- package/dist/core/AiAgent.d.ts +1126 -0
- package/dist/core/AiAgent.d.ts.map +1 -0
- package/dist/core/AiAgent.js +2037 -0
- package/dist/core/AiAgent.js.map +1 -0
- package/dist/core/api/ApiHelper.d.ts +433 -0
- package/dist/core/api/ApiHelper.d.ts.map +1 -0
- package/dist/core/api/ApiHelper.js +689 -0
- package/dist/core/api/ApiHelper.js.map +1 -0
- package/dist/core/api/CacheAdapter.d.ts +295 -0
- package/dist/core/api/CacheAdapter.d.ts.map +1 -0
- package/dist/core/api/CacheAdapter.js +298 -0
- package/dist/core/api/CacheAdapter.js.map +1 -0
- package/dist/core/auth/AnonymousAuthStrategy.d.ts +87 -0
- package/dist/core/auth/AnonymousAuthStrategy.d.ts.map +1 -0
- package/dist/core/auth/AnonymousAuthStrategy.js +257 -0
- package/dist/core/auth/AnonymousAuthStrategy.js.map +1 -0
- package/dist/core/auth/AuthProvider.d.ts +13 -0
- package/dist/core/auth/AuthProvider.d.ts.map +1 -0
- package/dist/core/auth/AuthProvider.js +2 -0
- package/dist/core/auth/AuthProvider.js.map +1 -0
- package/dist/core/auth/AuthStrategy.d.ts +74 -0
- package/dist/core/auth/AuthStrategy.d.ts.map +1 -0
- package/dist/core/auth/AuthStrategy.js +2 -0
- package/dist/core/auth/AuthStrategy.js.map +1 -0
- package/dist/core/auth/AuthenticationService.d.ts +226 -0
- package/dist/core/auth/AuthenticationService.d.ts.map +1 -0
- package/dist/core/auth/AuthenticationService.js +344 -0
- package/dist/core/auth/AuthenticationService.js.map +1 -0
- package/dist/core/auth/ClientCredentialsAuthStrategy.d.ts +62 -0
- package/dist/core/auth/ClientCredentialsAuthStrategy.d.ts.map +1 -0
- package/dist/core/auth/ClientCredentialsAuthStrategy.js +78 -0
- package/dist/core/auth/ClientCredentialsAuthStrategy.js.map +1 -0
- package/dist/core/auth/PKCEAuthStrategy.d.ts +136 -0
- package/dist/core/auth/PKCEAuthStrategy.d.ts.map +1 -0
- package/dist/core/auth/PKCEAuthStrategy.js +409 -0
- package/dist/core/auth/PKCEAuthStrategy.js.map +1 -0
- package/dist/core/auth/PreAuthStrategy.d.ts +101 -0
- package/dist/core/auth/PreAuthStrategy.d.ts.map +1 -0
- package/dist/core/auth/PreAuthStrategy.js +216 -0
- package/dist/core/auth/PreAuthStrategy.js.map +1 -0
- package/dist/core/auth/msal-browser.js +19683 -0
- package/dist/core/auth/msal-loader.d.ts +14 -0
- package/dist/core/auth/msal-loader.d.ts.map +1 -0
- package/dist/core/auth/msal-loader.js +43 -0
- package/dist/core/auth/msal-loader.js.map +1 -0
- package/dist/core/connection/Connection.d.ts +168 -0
- package/dist/core/connection/Connection.d.ts.map +1 -0
- package/dist/core/connection/Connection.js +290 -0
- package/dist/core/connection/Connection.js.map +1 -0
- package/dist/core/connection/ConnectionState.d.ts +11 -0
- package/dist/core/connection/ConnectionState.d.ts.map +1 -0
- package/dist/core/connection/ConnectionState.js +12 -0
- package/dist/core/connection/ConnectionState.js.map +1 -0
- package/dist/core/connection/Transport.d.ts +98 -0
- package/dist/core/connection/Transport.d.ts.map +1 -0
- package/dist/core/connection/Transport.js +27 -0
- package/dist/core/connection/Transport.js.map +1 -0
- package/dist/core/connection/WebSocketTransport.d.ts +65 -0
- package/dist/core/connection/WebSocketTransport.d.ts.map +1 -0
- package/dist/core/connection/WebSocketTransport.js +177 -0
- package/dist/core/connection/WebSocketTransport.js.map +1 -0
- package/dist/core/errors/SDKError.d.ts +27 -0
- package/dist/core/errors/SDKError.d.ts.map +1 -0
- package/dist/core/errors/SDKError.js +43 -0
- package/dist/core/errors/SDKError.js.map +1 -0
- package/dist/core/events/EventEmitter.d.ts +120 -0
- package/dist/core/events/EventEmitter.d.ts.map +1 -0
- package/dist/core/events/EventEmitter.js +183 -0
- package/dist/core/events/EventEmitter.js.map +1 -0
- package/dist/core/logging/LogLevel.d.ts +33 -0
- package/dist/core/logging/LogLevel.d.ts.map +1 -0
- package/dist/core/logging/LogLevel.js +40 -0
- package/dist/core/logging/LogLevel.js.map +1 -0
- package/dist/core/logging/Logger.d.ts +120 -0
- package/dist/core/logging/Logger.d.ts.map +1 -0
- package/dist/core/logging/Logger.js +204 -0
- package/dist/core/logging/Logger.js.map +1 -0
- package/dist/core/logging/globalLogger.d.ts +8 -0
- package/dist/core/logging/globalLogger.d.ts.map +1 -0
- package/dist/core/logging/globalLogger.js +12 -0
- package/dist/core/logging/globalLogger.js.map +1 -0
- package/dist/core/logging/types.d.ts +45 -0
- package/dist/core/logging/types.d.ts.map +1 -0
- package/dist/core/logging/types.js +2 -0
- package/dist/core/logging/types.js.map +1 -0
- package/dist/core/message/BaseMessageHandler.d.ts +208 -0
- package/dist/core/message/BaseMessageHandler.d.ts.map +1 -0
- package/dist/core/message/BaseMessageHandler.js +155 -0
- package/dist/core/message/BaseMessageHandler.js.map +1 -0
- package/dist/core/message/Message.d.ts +69 -0
- package/dist/core/message/Message.d.ts.map +1 -0
- package/dist/core/message/Message.js +131 -0
- package/dist/core/message/Message.js.map +1 -0
- package/dist/core/message/MessageProcessor.d.ts +51 -0
- package/dist/core/message/MessageProcessor.d.ts.map +1 -0
- package/dist/core/message/MessageProcessor.js +123 -0
- package/dist/core/message/MessageProcessor.js.map +1 -0
- package/dist/core/message/MessageTypes.d.ts +123 -0
- package/dist/core/message/MessageTypes.d.ts.map +1 -0
- package/dist/core/message/MessageTypes.js +106 -0
- package/dist/core/message/MessageTypes.js.map +1 -0
- package/dist/core/message/Transcript.d.ts +373 -0
- package/dist/core/message/Transcript.d.ts.map +1 -0
- package/dist/core/message/Transcript.js +355 -0
- package/dist/core/message/Transcript.js.map +1 -0
- package/dist/core/message/handlers/AgentMessageHandler.d.ts +26 -0
- package/dist/core/message/handlers/AgentMessageHandler.d.ts.map +1 -0
- package/dist/core/message/handlers/AgentMessageHandler.js +130 -0
- package/dist/core/message/handlers/AgentMessageHandler.js.map +1 -0
- package/dist/core/message/handlers/ChatHistoryHandler.d.ts +12 -0
- package/dist/core/message/handlers/ChatHistoryHandler.d.ts.map +1 -0
- package/dist/core/message/handlers/ChatHistoryHandler.js +49 -0
- package/dist/core/message/handlers/ChatHistoryHandler.js.map +1 -0
- package/dist/core/message/handlers/ErrorMessageHandler.d.ts +12 -0
- package/dist/core/message/handlers/ErrorMessageHandler.d.ts.map +1 -0
- package/dist/core/message/handlers/ErrorMessageHandler.js +49 -0
- package/dist/core/message/handlers/ErrorMessageHandler.js.map +1 -0
- package/dist/core/message/handlers/HeartbeatHandler.d.ts +12 -0
- package/dist/core/message/handlers/HeartbeatHandler.d.ts.map +1 -0
- package/dist/core/message/handlers/HeartbeatHandler.js +46 -0
- package/dist/core/message/handlers/HeartbeatHandler.js.map +1 -0
- package/dist/core/message/handlers/TokenRefreshHandler.d.ts +30 -0
- package/dist/core/message/handlers/TokenRefreshHandler.d.ts.map +1 -0
- package/dist/core/message/handlers/TokenRefreshHandler.js +84 -0
- package/dist/core/message/handlers/TokenRefreshHandler.js.map +1 -0
- package/dist/core/message/types.d.ts +107 -0
- package/dist/core/message/types.d.ts.map +1 -0
- package/dist/core/message/types.js +30 -0
- package/dist/core/message/types.js.map +1 -0
- package/dist/core/platform/HookContract.d.ts +112 -0
- package/dist/core/platform/HookContract.d.ts.map +1 -0
- package/dist/core/platform/HookContract.js +13 -0
- package/dist/core/platform/HookContract.js.map +1 -0
- package/dist/core/platform/PlatformComponentService.d.ts +40 -0
- package/dist/core/platform/PlatformComponentService.d.ts.map +1 -0
- package/dist/core/platform/PlatformComponentService.js +12 -0
- package/dist/core/platform/PlatformComponentService.js.map +1 -0
- package/dist/core/platform/PlatformScriptLoader.d.ts +41 -0
- package/dist/core/platform/PlatformScriptLoader.d.ts.map +1 -0
- package/dist/core/platform/PlatformScriptLoader.js +110 -0
- package/dist/core/platform/PlatformScriptLoader.js.map +1 -0
- package/dist/core/polyfills.d.ts +16 -0
- package/dist/core/polyfills.d.ts.map +1 -0
- package/dist/core/polyfills.js +168 -0
- package/dist/core/polyfills.js.map +1 -0
- package/dist/core/portal-initializer/PortalInitializer.d.ts +234 -0
- package/dist/core/portal-initializer/PortalInitializer.d.ts.map +1 -0
- package/dist/core/portal-initializer/PortalInitializer.js +636 -0
- package/dist/core/portal-initializer/PortalInitializer.js.map +1 -0
- package/dist/core/queue/MessageQueue.d.ts +277 -0
- package/dist/core/queue/MessageQueue.d.ts.map +1 -0
- package/dist/core/queue/MessageQueue.js +291 -0
- package/dist/core/queue/MessageQueue.js.map +1 -0
- package/dist/core/types/PortalTypes.d.ts +51 -0
- package/dist/core/types/PortalTypes.d.ts.map +1 -0
- package/dist/core/types/PortalTypes.js +8 -0
- package/dist/core/types/PortalTypes.js.map +1 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +82 -0
- package/dist/index.js.map +1 -0
- package/package.json +84 -0
|
@@ -0,0 +1,2037 @@
|
|
|
1
|
+
import { Connection } from './connection/Connection.js';
|
|
2
|
+
import { MessageQueue } from './queue/MessageQueue.js';
|
|
3
|
+
import { AuthenticationService } from './auth/AuthenticationService.js';
|
|
4
|
+
import { PKCEAuthStrategy } from './auth/PKCEAuthStrategy.js';
|
|
5
|
+
import { EventEmitter } from './events/EventEmitter.js';
|
|
6
|
+
import { ConnectionError, MessageError } from './errors/SDKError.js';
|
|
7
|
+
import { ApiHelper } from './api/ApiHelper.js';
|
|
8
|
+
import { createCacheAdapter } from './api/CacheAdapter.js';
|
|
9
|
+
import { MessageProcessor } from './message/MessageProcessor.js';
|
|
10
|
+
import { Message } from './message/Message.js';
|
|
11
|
+
import { PERSONA, ROLE } from './message/types.js';
|
|
12
|
+
import { TokenRefreshHandler } from './message/handlers/TokenRefreshHandler.js';
|
|
13
|
+
import { Logger } from './logging/Logger.js';
|
|
14
|
+
import { LogLevel } from './logging/LogLevel.js';
|
|
15
|
+
import { createGracefulDisconnectMessage, createTokenMessage, createContextMessage } from './message/MessageTypes.js';
|
|
16
|
+
import { Transcript } from './message/Transcript.js';
|
|
17
|
+
import { PortalInitializer } from './portal-initializer/PortalInitializer.js';
|
|
18
|
+
import { loadPlatformScript, deriveEnvironment } from './platform/PlatformScriptLoader.js';
|
|
19
|
+
/**
|
|
20
|
+
* Cache key prefix for context storage
|
|
21
|
+
*/
|
|
22
|
+
const CONTEXT_CACHE_KEY_PREFIX = 'egain_aiagent_context_';
|
|
23
|
+
/** Session-scoped cache key prefix for profile list (restart reuse). Suffix: agentId_portalId. */
|
|
24
|
+
const PIPELINE_PROFILES_CACHE_KEY_PREFIX = 'eg_profiles_';
|
|
25
|
+
/**
|
|
26
|
+
* Main class for interacting with the eGain AI Agent platform.
|
|
27
|
+
*
|
|
28
|
+
* The AiAgent class provides:
|
|
29
|
+
* - WebSocket connection management with automatic reconnection
|
|
30
|
+
* - Message queuing when offline
|
|
31
|
+
* - Event-driven communication
|
|
32
|
+
* - Transcript management
|
|
33
|
+
* - Context persistence
|
|
34
|
+
*
|
|
35
|
+
* **Initialization flows**
|
|
36
|
+
*
|
|
37
|
+
* After authentication, one of two paths runs:
|
|
38
|
+
*
|
|
39
|
+
* - **Direct flow** (non–contact-center agents): fetches session, creates the WebSocket connection,
|
|
40
|
+
* emits `initialized`. With {@link AiAgentConfig.autoConnect}, `connect()` runs automatically.
|
|
41
|
+
* - **Contact Center (CC) flow** (contact-center agents, per API `agentType` / authenticated agents
|
|
42
|
+
* with legacy empty type): runs a REST-only portal → (optional agent) → profile pipeline, then
|
|
43
|
+
* emits `initialized`. The WebSocket is created when you call `connect()` (or automatically if
|
|
44
|
+
* `autoConnect` is true after the pipeline completes).
|
|
45
|
+
*
|
|
46
|
+
* **Flow A (specific agent)** — Use the target agent ID in {@link AiAgentConfig.id}. CC pipeline:
|
|
47
|
+
* portal selection → profile selection.
|
|
48
|
+
*
|
|
49
|
+
* **Flow B (default agent / agent selection)** — Set `initParams: { isDefaultAgent: "true" }`.
|
|
50
|
+
* CC pipeline: portal → agent → profile. The selected agent becomes the chat identity
|
|
51
|
+
* (`resolvedAgentId`); subsequent session and chat use that ID, not the bootstrap `config.id`.
|
|
52
|
+
*
|
|
53
|
+
* When the CC pipeline has multiple options at a step, it emits `portalsAvailable`,
|
|
54
|
+
* `agentsAvailable`, or `profilesAvailable`. Call {@link AiAgent.selectPortal},
|
|
55
|
+
* {@link AiAgent.selectAgent}, or {@link AiAgent.selectUserProfile} to continue. After the
|
|
56
|
+
* `initialized` event, call {@link AiAgent.connect} unless `autoConnect` already connected you.
|
|
57
|
+
*
|
|
58
|
+
* @example Direct flow (typical non–Contact Center agent)
|
|
59
|
+
* ```typescript
|
|
60
|
+
* import { AiAgent } from "@eGain/ai-agent-sdk";
|
|
61
|
+
*
|
|
62
|
+
* const agent = new AiAgent({
|
|
63
|
+
* id: "agent-id",
|
|
64
|
+
* endpoint: "https://api.egain.com",
|
|
65
|
+
* auth: { type: "pre-auth", accessToken: "your-access-token" },
|
|
66
|
+
* autoConnect: true,
|
|
67
|
+
* });
|
|
68
|
+
*
|
|
69
|
+
* agent.on("agentMessage", (event) => {
|
|
70
|
+
* console.log("Agent:", event.payload.message?.content);
|
|
71
|
+
* });
|
|
72
|
+
*
|
|
73
|
+
* await agent.initialize();
|
|
74
|
+
* await agent.send("Hello!");
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* @example Contact Center flow (register handlers before `initialize`)
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const agent = new AiAgent({
|
|
80
|
+
* id: "agent-id",
|
|
81
|
+
* endpoint: "https://api.egain.com",
|
|
82
|
+
* auth: { type: "pkce", config: { ... } },
|
|
83
|
+
* initParams: { userid: "user-123" },
|
|
84
|
+
* });
|
|
85
|
+
*
|
|
86
|
+
* agent.on("portalsAvailable", (e) => agent.selectPortal(e.payload.portals[0]));
|
|
87
|
+
* agent.on("agentsAvailable", (e) => agent.selectAgent(e.payload.agents[0]));
|
|
88
|
+
* agent.on("profilesAvailable", (e) => agent.selectUserProfile(e.payload.profiles[0]));
|
|
89
|
+
* agent.on("initialized", async () => {
|
|
90
|
+
* await agent.connect();
|
|
91
|
+
* });
|
|
92
|
+
* await agent.initialize();
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* @example With context
|
|
96
|
+
* ```typescript
|
|
97
|
+
* import { AiAgent, createContextMessage } from "@eGain/ai-agent-sdk";
|
|
98
|
+
*
|
|
99
|
+
* await agent.send(createContextMessage({
|
|
100
|
+
* context: { userId: "123", accountType: "premium" },
|
|
101
|
+
* }));
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* @category Core
|
|
105
|
+
* @see {@link AiAgentConfig} for configuration options
|
|
106
|
+
* @see {@link AgentEvents} for available events
|
|
107
|
+
*/
|
|
108
|
+
export class AiAgent extends EventEmitter {
|
|
109
|
+
constructor(config) {
|
|
110
|
+
super();
|
|
111
|
+
/** True when the agent completed the portal initialization pipeline (needed for restart guard). */
|
|
112
|
+
this.completedPortalPipeline = false;
|
|
113
|
+
// --- Platform connector state (Phase 2) ---
|
|
114
|
+
this.callerInfo = null;
|
|
115
|
+
this.userContext = null;
|
|
116
|
+
this.platformToken = null;
|
|
117
|
+
this.isPlatformAuthenticated = false;
|
|
118
|
+
this.conversationId = null;
|
|
119
|
+
this.filterTags = {};
|
|
120
|
+
this.callTranscript = [];
|
|
121
|
+
this.userDetails = null;
|
|
122
|
+
/** True after `addCustomAuthScopes` merged scopes into `config.scopes` (PKCE/hooks must use that list). */
|
|
123
|
+
this.authScopesAugmentedByPlatform = false;
|
|
124
|
+
/**
|
|
125
|
+
* Post-authentication callback. Runs the portal initializer when
|
|
126
|
+
* {@link shouldRunPortalInitializationPipeline} is true; otherwise session + connection (direct flow).
|
|
127
|
+
* @param accessToken - The access token to use for authentication
|
|
128
|
+
*/
|
|
129
|
+
this.onAuthComplete = async (accessToken) => {
|
|
130
|
+
const runPortalPipeline = this.shouldRunPortalInitializationPipeline();
|
|
131
|
+
this.logger.debug('onAuthComplete callback invoked', {
|
|
132
|
+
agentId: this.config.id,
|
|
133
|
+
agentType: this.agentDetails?.agentType,
|
|
134
|
+
runPortalPipeline,
|
|
135
|
+
instanceId: this._instanceId,
|
|
136
|
+
});
|
|
137
|
+
await this.fetchUserOrCustomerDetails(accessToken);
|
|
138
|
+
if (runPortalPipeline) {
|
|
139
|
+
this.logger.info('Running portal initializer pipeline', { agentId: this.config.id });
|
|
140
|
+
await this.runPortalInitializerPipeline(accessToken);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
this.logger.info('Running direct initialization pipeline', { agentId: this.config.id });
|
|
144
|
+
await this.runDirectInitializationAfterAuth(accessToken);
|
|
145
|
+
}
|
|
146
|
+
this.logger.debug('onAuthComplete completed', {
|
|
147
|
+
agentId: this.resolvedAgentId,
|
|
148
|
+
isInitialized: this.isInitialized,
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
this.isFlushingQueue = false;
|
|
152
|
+
this.isInitialized = false;
|
|
153
|
+
this.config = config;
|
|
154
|
+
this.resolvedAgentId = config.id;
|
|
155
|
+
this.isAgentSelectionMode = (config.initParams?.isDefaultAgent?.toLowerCase() ?? '') === 'true';
|
|
156
|
+
this.initParams = config.initParams ?? {};
|
|
157
|
+
// Debug: track instance identity
|
|
158
|
+
this._instanceId = Math.random().toString(36).substring(7);
|
|
159
|
+
// Validate configuration
|
|
160
|
+
if (!config.endpoint) {
|
|
161
|
+
throw new Error('Endpoint is required');
|
|
162
|
+
}
|
|
163
|
+
if (!config.id) {
|
|
164
|
+
throw new Error('Agent ID is required');
|
|
165
|
+
}
|
|
166
|
+
// Store sessionId from config if provided
|
|
167
|
+
if (config.sessionId !== undefined) {
|
|
168
|
+
this.sessionId = config.sessionId;
|
|
169
|
+
// Note: Logger will be initialized below and will use this.sessionId in contextProvider
|
|
170
|
+
}
|
|
171
|
+
// Initialize logger
|
|
172
|
+
if (config.logger) {
|
|
173
|
+
this.logger = config.logger;
|
|
174
|
+
// Note: If a custom logger is provided, sessionId won't be automatically included
|
|
175
|
+
// unless the custom logger has its own contextProvider configured
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
this.logger = new Logger({
|
|
179
|
+
level: config.logLevel ?? LogLevel.INFO,
|
|
180
|
+
enableConsole: config.enableLogging ?? true,
|
|
181
|
+
name: `AiAgent:${config.id}`,
|
|
182
|
+
contextProvider: () => ({
|
|
183
|
+
sessionId: this.sessionId,
|
|
184
|
+
}),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// Log if sessionId was provided in config
|
|
188
|
+
if (config.sessionId !== undefined) {
|
|
189
|
+
this.logger.debug('SessionId provided in config, will skip network fetch', {
|
|
190
|
+
sessionId: this.sessionId,
|
|
191
|
+
agentId: config.id
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// Initialize message queue
|
|
195
|
+
this.messageQueue = new MessageQueue(config.maxQueueSize ?? 1000);
|
|
196
|
+
// Initialize message processor
|
|
197
|
+
this.messageProcessor = new MessageProcessor(this.logger.createChild('MessageProcessor'));
|
|
198
|
+
// Initialize transcript
|
|
199
|
+
this.transcript = new Transcript(config.transcriptConfig);
|
|
200
|
+
// Create authentication service - handles all input types internally
|
|
201
|
+
// Pass cache config so AnonymousAuthStrategy can use it for caching metadata
|
|
202
|
+
this.authService = new AuthenticationService(config.auth, this.logger.createChild('AuthenticationService'), config.cache);
|
|
203
|
+
// Initialize context cache adapter
|
|
204
|
+
// Use custom adapter if provided in cache config, otherwise create based on storage type
|
|
205
|
+
if (config.cache?.adapter) {
|
|
206
|
+
this.contextCacheAdapter = config.cache.adapter;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
this.contextCacheAdapter = createCacheAdapter(config.cache?.storageType || 'session');
|
|
210
|
+
}
|
|
211
|
+
// Replace default TokenRefreshHandler with one that has callbacks for token refresh
|
|
212
|
+
this.setupTokenRefreshHandler();
|
|
213
|
+
this.logger.debug('AiAgent instance created', { agentId: config.id, endpoint: config.endpoint });
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Setup TokenRefreshHandler with callbacks for automatic token refresh
|
|
217
|
+
* Replaces the default handler with one that can actually perform token refresh
|
|
218
|
+
*/
|
|
219
|
+
setupTokenRefreshHandler() {
|
|
220
|
+
const handlers = this.messageProcessor.getHandlers();
|
|
221
|
+
const defaultTokenHandler = handlers.find((h) => h instanceof TokenRefreshHandler);
|
|
222
|
+
if (defaultTokenHandler) {
|
|
223
|
+
// Remove default handler
|
|
224
|
+
this.messageProcessor.removeHandler(defaultTokenHandler);
|
|
225
|
+
// Add handler with callbacks
|
|
226
|
+
const tokenRefreshHandler = new TokenRefreshHandler({
|
|
227
|
+
getAccessToken: async () => {
|
|
228
|
+
const token = await this.authService.getToken();
|
|
229
|
+
if (!token) {
|
|
230
|
+
throw new Error('Failed to get access token for refresh');
|
|
231
|
+
}
|
|
232
|
+
return token;
|
|
233
|
+
},
|
|
234
|
+
sendToConnection: async (payload) => {
|
|
235
|
+
if (!this.connection?.isConnected()) {
|
|
236
|
+
this.logger.warn('Cannot send token refresh: connection not available');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const payloadString = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
240
|
+
await this.connection.send(payloadString);
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
// Add at priority 0 (highest priority)
|
|
244
|
+
this.messageProcessor.addHandler(tokenRefreshHandler, 0);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
parseQueryScopes() {
|
|
248
|
+
const raw = this.initParams.scopes?.trim();
|
|
249
|
+
if (!raw)
|
|
250
|
+
return [];
|
|
251
|
+
return raw
|
|
252
|
+
.split(',')
|
|
253
|
+
.map((s) => s.trim())
|
|
254
|
+
.filter((s) => s.length > 0);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Base OAuth resource scopes before platform augmentation: comma-separated `initParams.scopes`
|
|
258
|
+
* overrides `config.scopes` and defaults. Used to seed `addCustomAuthScopes`. For PKCE and hooks
|
|
259
|
+
* after the platform merges scopes, use {@link getAuthScopesForFlow}.
|
|
260
|
+
*/
|
|
261
|
+
resolveEffectiveAuthScopes() {
|
|
262
|
+
const fromQuery = this.parseQueryScopes();
|
|
263
|
+
if (fromQuery.length > 0) {
|
|
264
|
+
return [...fromQuery];
|
|
265
|
+
}
|
|
266
|
+
if (this.config.scopes && this.config.scopes.length > 0) {
|
|
267
|
+
return [...this.config.scopes];
|
|
268
|
+
}
|
|
269
|
+
const scopes = ['knowledge.portalmgr.manage', 'core.aiservices.read'];
|
|
270
|
+
if (this.agentDetails?.userType === 'customer') {
|
|
271
|
+
scopes.push('core.customermgr.read');
|
|
272
|
+
}
|
|
273
|
+
return scopes;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Scopes used for PKCE, AuthenticationService.initialize, and HookContract.getAuthScopes.
|
|
277
|
+
* After the platform connector augments scopes into `config.scopes`, that merged list wins.
|
|
278
|
+
*/
|
|
279
|
+
getAuthScopesForFlow() {
|
|
280
|
+
if (this.authScopesAugmentedByPlatform && this.config.scopes && this.config.scopes.length > 0) {
|
|
281
|
+
return [...this.config.scopes];
|
|
282
|
+
}
|
|
283
|
+
return this.resolveEffectiveAuthScopes();
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Initialize the agent. Must be called after construction and awaited before use.
|
|
287
|
+
*
|
|
288
|
+
* Authenticates (falls back to {@link AnonymousAuthStrategy} if no auth is configured), then:
|
|
289
|
+
*
|
|
290
|
+
* - **Direct flow:** fetches session, creates the WebSocket connection, emits `initialized`.
|
|
291
|
+
* With `autoConnect`, opens the WebSocket automatically.
|
|
292
|
+
* - **Contact Center flow:** runs portal / agent / profile selection over REST only (no WebSocket
|
|
293
|
+
* yet). May emit `portalsAvailable`, `agentsAvailable`, or `profilesAvailable` — call the
|
|
294
|
+
* corresponding `select*` method. Then emits `initialized`. Call {@link AiAgent.connect}
|
|
295
|
+
* afterward (or rely on `autoConnect` after the pipeline completes).
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* ```typescript
|
|
299
|
+
* const agent = new AiAgent({ id: 'agent-id', endpoint: 'https://...' });
|
|
300
|
+
* await agent.initialize();
|
|
301
|
+
* // Direct flow: often already connected if autoConnect. CC flow: connect after `initialized`.
|
|
302
|
+
* await agent.connect();
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
async initialize() {
|
|
306
|
+
if (this.isInitialized) {
|
|
307
|
+
this.logger.debug('Agent already initialized', { agentId: this.config.id });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
this.logger.debug('Initializing agent', { agentId: this.config.id });
|
|
311
|
+
try {
|
|
312
|
+
this.authScopesAugmentedByPlatform = false;
|
|
313
|
+
// Get deployment info (use cached if getAgentDetails was called first)
|
|
314
|
+
if (!this.deploymentInfo) {
|
|
315
|
+
this.deploymentInfo = await ApiHelper.getDeploymentInfo(this.config.endpoint);
|
|
316
|
+
}
|
|
317
|
+
if (!this.deploymentInfo) {
|
|
318
|
+
const error = new Error('Deployment information not found');
|
|
319
|
+
this.logger.error('Failed to initialize: deployment information not found', error, { agentId: this.config.id });
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
// Set up apiHelper if not already set (may have been set by getAgentDetails)
|
|
323
|
+
if (!this.apiHelper) {
|
|
324
|
+
this.apiHelper = new ApiHelper({
|
|
325
|
+
apiDomain: this.deploymentInfo.apiDomain,
|
|
326
|
+
cache: this.config.cache,
|
|
327
|
+
getToken: () => this.authService.getToken(),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
this.logger.debug('Deployment info retrieved', { apiDomain: this.deploymentInfo.apiDomain });
|
|
331
|
+
// Set up token expiring callback to emit tokenExpiring event
|
|
332
|
+
this.authService.setTokenExpiringCallback((expiresAt) => {
|
|
333
|
+
this.logger.debug('Token expiring callback triggered', { expiresAt, agentId: this.config.id });
|
|
334
|
+
this.emit('tokenExpiring', this.createAgentEventResponse('tokenExpiring', {
|
|
335
|
+
reason: 'expiring',
|
|
336
|
+
expiresAt,
|
|
337
|
+
}));
|
|
338
|
+
});
|
|
339
|
+
if (!this.agentDetails) {
|
|
340
|
+
// Initialize auth service with deployment info if not already initialized
|
|
341
|
+
// Default strategy is already anonymous, so we can get token from current strategy
|
|
342
|
+
if (!this.authService.getIsInitialized()) {
|
|
343
|
+
await this.authService.initialize({
|
|
344
|
+
deploymentInfo: this.deploymentInfo,
|
|
345
|
+
// No postAuthentication - we don't want session ID or connection yet
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
const accessToken = await this.authService.getToken();
|
|
349
|
+
this.agentDetails = await this.fetchAgentDetails(accessToken);
|
|
350
|
+
}
|
|
351
|
+
// Load platform connector script if applicable (before auth so scopes can be augmented)
|
|
352
|
+
const platform = this.initParams.platform?.toLowerCase();
|
|
353
|
+
const shouldLoadPlatformScript = platform != null &&
|
|
354
|
+
platform !== 'standalone' &&
|
|
355
|
+
platform !== 'test' &&
|
|
356
|
+
/^[a-zA-Z]+$/.test(platform) &&
|
|
357
|
+
this.agentDetails?.agentType === 'contact-center';
|
|
358
|
+
if (shouldLoadPlatformScript) {
|
|
359
|
+
await this.loadAndInitializePlatform();
|
|
360
|
+
}
|
|
361
|
+
// Branch based on whether agent requires authentication
|
|
362
|
+
if (this.agentDetails?.isAuthenticated) {
|
|
363
|
+
const effectiveScopes = this.getAuthScopesForFlow();
|
|
364
|
+
// Agent requires authentication - use PKCE
|
|
365
|
+
// Check if current strategy is anonymous, and switch to PKCE if needed
|
|
366
|
+
// Otherwise keep the same strategy
|
|
367
|
+
if (this.authService.isAnonymousStrategy()) {
|
|
368
|
+
// Build PKCE config from deployment info and agent details
|
|
369
|
+
let pkceConfig;
|
|
370
|
+
// Check if PKCE config was provided in the original auth config
|
|
371
|
+
if (this.config.auth && typeof this.config.auth === 'object' && 'type' in this.config.auth && this.config.auth.type === 'pkce') {
|
|
372
|
+
pkceConfig = this.config.auth.config;
|
|
373
|
+
if (this.parseQueryScopes().length > 0 || this.authScopesAugmentedByPlatform) {
|
|
374
|
+
pkceConfig = { ...pkceConfig, scopes: [...effectiveScopes] };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
// Build PKCE config from deployment info and agent details
|
|
379
|
+
try {
|
|
380
|
+
const egClientId = this.initParams.egclientid || this.initParams.egclientId || this.initParams.egClientId;
|
|
381
|
+
const localLogin = this.initParams.localLogin === 'true' || undefined;
|
|
382
|
+
pkceConfig = await PKCEAuthStrategy.buildConfigFromDeploymentInfo(this.deploymentInfo, this.agentDetails, this.config.endpoint, effectiveScopes, this.logger.createChild('PKCEAuthStrategy'), this.config.authScheme, egClientId, localLogin);
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
386
|
+
this.logger.error('Failed to build PKCE config from deployment info', err, { agentId: this.config.id });
|
|
387
|
+
throw new Error(`Failed to build PKCE configuration: ${err.message}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Switch to PKCE strategy with postAuthentication callback
|
|
391
|
+
// postAuthentication will be called once authentication type is confirmed
|
|
392
|
+
this.logger.debug('Switching from anonymous to PKCE strategy', { agentId: this.config.id });
|
|
393
|
+
await this.authService.switchStrategyTo(pkceConfig, this.onAuthComplete.bind(this));
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
this.logger.debug('Current strategy is not anonymous, keeping existing strategy', {
|
|
397
|
+
agentId: this.config.id,
|
|
398
|
+
currentType: this.authService.getAuthenticationType()
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
// Initialize and authenticate - postAuthentication callback will be called after authentication completes
|
|
402
|
+
this.logger.debug('Agent requires authentication, initializing PKCE flow', { agentId: this.config.id });
|
|
403
|
+
await this.authService.initialize({
|
|
404
|
+
deploymentInfo: this.deploymentInfo,
|
|
405
|
+
postAuthentication: this.onAuthComplete,
|
|
406
|
+
scopes: effectiveScopes,
|
|
407
|
+
userType: this.agentDetails?.userType,
|
|
408
|
+
});
|
|
409
|
+
await this.authService.authenticate();
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
// Agent doesn't require authentication - use anonymous strategy
|
|
413
|
+
this.logger.debug('Agent does not require authentication, using anonymous strategy', { agentId: this.config.id });
|
|
414
|
+
// Complete initialization manually (get session, create connection)
|
|
415
|
+
// TODO: POST AUTHENTICATION CALLBACK
|
|
416
|
+
const accessToken = await this.authService.getToken();
|
|
417
|
+
await this.onAuthComplete(accessToken);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
// Ensure we don't mark as initialized if initialization failed
|
|
422
|
+
this.isInitialized = false;
|
|
423
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
424
|
+
this.logger.error('Failed to initialize agent', err, { agentId: this.config.id });
|
|
425
|
+
throw err;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Build the HookContract by closing over `this`.
|
|
430
|
+
* All getters return live state (not stale snapshots).
|
|
431
|
+
*/
|
|
432
|
+
buildHookContract() {
|
|
433
|
+
return {
|
|
434
|
+
getTranscript: () => [...this.callTranscript],
|
|
435
|
+
getInitParams: () => ({ ...this.initParams }),
|
|
436
|
+
getAgentDetails: () => this.agentDetails,
|
|
437
|
+
getMsalAccessToken: () => this.authService.getToken(),
|
|
438
|
+
getDeploymentInfo: () => this.deploymentInfo,
|
|
439
|
+
getPlatformType: () => this.initParams.platform ?? null,
|
|
440
|
+
getEnvironment: () => deriveEnvironment(this.initParams.env),
|
|
441
|
+
getUserId: () => this.initParams.userid ?? this.initParams.userId ?? null,
|
|
442
|
+
getUserContext: () => this.userContext,
|
|
443
|
+
getConversationId: () => this.conversationId,
|
|
444
|
+
getAuthScopes: () => this.getAuthScopesForFlow(),
|
|
445
|
+
getTenantId: () => this.deploymentInfo?.tenantId ?? null,
|
|
446
|
+
getSelectedPortal: () => this.lastSelectedPortal ?? null,
|
|
447
|
+
getCallerInfo: () => this.callerInfo,
|
|
448
|
+
addToTranscript: (entry) => {
|
|
449
|
+
const newEntry = {
|
|
450
|
+
sender: entry.sender,
|
|
451
|
+
content: entry.content,
|
|
452
|
+
timestamp: entry.timestamp ?? new Date(),
|
|
453
|
+
};
|
|
454
|
+
this.callTranscript.push(newEntry);
|
|
455
|
+
this.emit('callTranscriptUpdate', this.createAgentEventResponse('callTranscriptUpdate', {
|
|
456
|
+
entry: newEntry,
|
|
457
|
+
}));
|
|
458
|
+
},
|
|
459
|
+
setCallerInfo: (info) => {
|
|
460
|
+
this.callerInfo = info;
|
|
461
|
+
this.emit('callerInfoUpdate', this.createAgentEventResponse('callerInfoUpdate', {
|
|
462
|
+
callerInfo: info,
|
|
463
|
+
}));
|
|
464
|
+
},
|
|
465
|
+
setPlatformAuthenticated: (v) => { this.isPlatformAuthenticated = v; },
|
|
466
|
+
setPlatformToken: (token) => { this.platformToken = token; },
|
|
467
|
+
setConversationId: (id) => {
|
|
468
|
+
this.conversationId = id;
|
|
469
|
+
this.emit('conversationIdUpdate', this.createAgentEventResponse('conversationIdUpdate', {
|
|
470
|
+
conversationId: id,
|
|
471
|
+
}));
|
|
472
|
+
},
|
|
473
|
+
setUserContext: (ctx) => {
|
|
474
|
+
this.userContext = { ...this.userContext, ...ctx };
|
|
475
|
+
this.emit('userContextUpdate', this.createAgentEventResponse('userContextUpdate', {
|
|
476
|
+
userContext: this.userContext,
|
|
477
|
+
}));
|
|
478
|
+
},
|
|
479
|
+
setUserFilterTags: (tags) => {
|
|
480
|
+
this.filterTags = tags;
|
|
481
|
+
this.emit('filterTagsUpdate', this.createAgentEventResponse('filterTagsUpdate', {
|
|
482
|
+
filterTags: tags,
|
|
483
|
+
}));
|
|
484
|
+
},
|
|
485
|
+
subscribeToAgentWidgetActions: (cb) => {
|
|
486
|
+
const handler = (event) => { cb(event?.type ?? 'unknown', event?.payload); };
|
|
487
|
+
this.on('message', handler);
|
|
488
|
+
return () => { this.off('message', handler); };
|
|
489
|
+
},
|
|
490
|
+
onUserMessage: (msg) => { this.send(msg); },
|
|
491
|
+
onSourceClick: (source) => { this.emit('message', this.createAgentEventResponse('message', { type: 'sourceClick', source })); },
|
|
492
|
+
onIntentConfirm: (intent) => { this.emit('message', this.createAgentEventResponse('message', { type: 'intentConfirm', intent })); },
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Load the platform connector script and wire up the HookContract.
|
|
497
|
+
* Called from initialize() when a non-standalone platform is detected.
|
|
498
|
+
*/
|
|
499
|
+
async loadAndInitializePlatform() {
|
|
500
|
+
const platform = this.initParams.platform.toLowerCase();
|
|
501
|
+
const environment = deriveEnvironment(this.deploymentInfo?.apiDomain, this.initParams.env);
|
|
502
|
+
this.logger.info('Loading platform connector', { platform, environment });
|
|
503
|
+
await loadPlatformScript({
|
|
504
|
+
platform,
|
|
505
|
+
baseUrl: environment,
|
|
506
|
+
overrideUrl: this.config.platformScriptUrl,
|
|
507
|
+
logger: this.logger,
|
|
508
|
+
});
|
|
509
|
+
this.platformComponentService =
|
|
510
|
+
(typeof window !== 'undefined' ? window : globalThis).PlatformComponentService ?? undefined;
|
|
511
|
+
if (!this.platformComponentService) {
|
|
512
|
+
throw new Error(`Failed to load platform connector script for '${platform}'`);
|
|
513
|
+
}
|
|
514
|
+
this.hookContract = this.buildHookContract();
|
|
515
|
+
if (this.platformComponentService.setHookContract) {
|
|
516
|
+
this.platformComponentService.setHookContract(this.hookContract);
|
|
517
|
+
}
|
|
518
|
+
if (this.platformComponentService.loadCustomHook) {
|
|
519
|
+
this.platformComponentService.loadCustomHook(this.hookContract);
|
|
520
|
+
}
|
|
521
|
+
if (this.platformComponentService.addCustomAuthScopes) {
|
|
522
|
+
const currentScopes = this.resolveEffectiveAuthScopes();
|
|
523
|
+
const augmentedScopes = await this.platformComponentService.addCustomAuthScopes(currentScopes);
|
|
524
|
+
if (augmentedScopes && Array.isArray(augmentedScopes)) {
|
|
525
|
+
this.config.scopes = augmentedScopes;
|
|
526
|
+
this.authScopesAugmentedByPlatform = true;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
this.logger.info('Platform connector initialized', { platform });
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Whether to run the portal-based initializer (portals → optional agents → profiles) vs direct session setup.
|
|
533
|
+
* True when portal-related init params or agent configuration indicate a portal-trained / contact-center flow.
|
|
534
|
+
*/
|
|
535
|
+
shouldRunPortalInitializationPipeline() {
|
|
536
|
+
const q = this.initParams;
|
|
537
|
+
if ((q.portalIds ?? q.portalids ?? '').trim().length > 0) {
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
const portals = this.agentDetails?.portals;
|
|
541
|
+
if (Array.isArray(portals) && portals.length > 0) {
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
// if (this.agentDetails?.agentType === 'contact-center') {
|
|
545
|
+
// return true;
|
|
546
|
+
// }
|
|
547
|
+
if ((q.isDefaultAgent?.toLowerCase() ?? '') === 'true') {
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Session + connection + `initialized` for the non-portal path after auth.
|
|
554
|
+
*/
|
|
555
|
+
async runDirectInitializationAfterAuth(accessToken) {
|
|
556
|
+
this.isInitialized = true;
|
|
557
|
+
this.sessionId = await this.getSessionId(accessToken);
|
|
558
|
+
await this.createConnection(this.sessionId);
|
|
559
|
+
const agentDetails = this.agentDetails ?? (this.apiHelper && (await this.fetchAgentDetails(accessToken)));
|
|
560
|
+
const payload = {
|
|
561
|
+
agent: agentDetails ?? { agentId: this.resolvedAgentId, name: 'Unknown' },
|
|
562
|
+
};
|
|
563
|
+
this.emit('initialized', this.createAgentEventResponse('initialized', payload));
|
|
564
|
+
if (this.config.autoConnect) {
|
|
565
|
+
await this.connect();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
async runPortalInitializerPipeline(accessToken) {
|
|
569
|
+
if (this.platformComponentService?.initPlatform) {
|
|
570
|
+
await this.platformComponentService.initPlatform(this.hookContract);
|
|
571
|
+
}
|
|
572
|
+
this.portalInitializer = new PortalInitializer({
|
|
573
|
+
agentId: this.config.id,
|
|
574
|
+
apiHelper: this.apiHelper,
|
|
575
|
+
logger: this.logger.createChild('PortalInitializer'),
|
|
576
|
+
authService: this.authService,
|
|
577
|
+
initParams: this.initParams,
|
|
578
|
+
platformComponentService: this.platformComponentService,
|
|
579
|
+
hookContract: this.hookContract,
|
|
580
|
+
emit: (type, event) => {
|
|
581
|
+
if (type === 'initialized') {
|
|
582
|
+
const payload = event?.payload;
|
|
583
|
+
if (payload?.agent) {
|
|
584
|
+
this.resolvedAgentId = payload.agent.agentId ?? payload.agent.id ?? this.resolvedAgentId;
|
|
585
|
+
}
|
|
586
|
+
this.completedPortalPipeline = true;
|
|
587
|
+
this.lastSelectedPortal = payload?.portal;
|
|
588
|
+
if (payload?.availableProfiles) {
|
|
589
|
+
this.cachedProfiles = payload.availableProfiles;
|
|
590
|
+
const portalId = payload?.portal?.id;
|
|
591
|
+
const profilesCacheKey = this.getPipelineProfilesCacheKey(portalId);
|
|
592
|
+
const existingEntry = this.contextCacheAdapter.get(profilesCacheKey);
|
|
593
|
+
if (!existingEntry) {
|
|
594
|
+
this.contextCacheAdapter.set(profilesCacheKey, {
|
|
595
|
+
value: payload.availableProfiles,
|
|
596
|
+
timestamp: Date.now(),
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
this.isInitialized = true;
|
|
601
|
+
}
|
|
602
|
+
this.emit(type, event);
|
|
603
|
+
if (type === 'initialized' && this.config.autoConnect) {
|
|
604
|
+
this.connect();
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
createAgentEventResponse: (type, payload) => this.createAgentEventResponse(type, payload),
|
|
608
|
+
isAgentSelectionMode: this.isAgentSelectionMode,
|
|
609
|
+
agentDetails: this.agentDetails,
|
|
610
|
+
pipelineCache: {
|
|
611
|
+
adapter: this.contextCacheAdapter,
|
|
612
|
+
profilesKey: (portalId) => this.getPipelineProfilesCacheKey(portalId),
|
|
613
|
+
ttl: this.config.cache?.ttl ?? 60 * 60 * 1000, // 1 hour
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
void this.portalInitializer.start();
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Fetch user or customer details after authentication.
|
|
620
|
+
* Determination: if userType is 'customer', fetches customer details; otherwise fetches user details.
|
|
621
|
+
* Best-effort — logs a warning on failure but does not throw.
|
|
622
|
+
*/
|
|
623
|
+
async fetchUserOrCustomerDetails(accessToken) {
|
|
624
|
+
if (!this.apiHelper)
|
|
625
|
+
return;
|
|
626
|
+
try {
|
|
627
|
+
const userType = this.agentDetails?.userType || 'user';
|
|
628
|
+
this.userDetails = userType === 'customer'
|
|
629
|
+
? await this.apiHelper.getCustomerDetails({ authToken: accessToken })
|
|
630
|
+
: await this.apiHelper.getUserDetails({ authToken: accessToken });
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
this.logger.warn('Failed to fetch user/customer details', {
|
|
634
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
635
|
+
agentId: this.config.id,
|
|
636
|
+
});
|
|
637
|
+
this.userDetails = null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Fetch agent details from API
|
|
642
|
+
* @param accessToken - The access token to use for authentication
|
|
643
|
+
* @returns The agent details
|
|
644
|
+
*/
|
|
645
|
+
async fetchAgentDetails(accessToken) {
|
|
646
|
+
if (this.agentDetails) {
|
|
647
|
+
this.backfillAgentDetailsId(this.agentDetails);
|
|
648
|
+
return this.agentDetails;
|
|
649
|
+
}
|
|
650
|
+
accessToken = accessToken ?? await this.authService.getToken() ?? null;
|
|
651
|
+
if (!accessToken) {
|
|
652
|
+
const error = new Error('Access token not found.');
|
|
653
|
+
this.logger.error('Failed to get agent details: access token not found', error, { agentId: this.config.id });
|
|
654
|
+
throw error;
|
|
655
|
+
}
|
|
656
|
+
this.logger.debug('Fetching agent details', { agentId: this.config.id });
|
|
657
|
+
this.agentDetails = await this.apiHelper?.getAiAgentDetails({
|
|
658
|
+
agentId: this.config.id,
|
|
659
|
+
authToken: accessToken,
|
|
660
|
+
});
|
|
661
|
+
this.backfillAgentDetailsId(this.agentDetails);
|
|
662
|
+
this.logger.debug('Agent details retrieved', { agentId: this.config.id });
|
|
663
|
+
return this.agentDetails;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Ensures agent details have agentId from the request context when API omits it.
|
|
667
|
+
*
|
|
668
|
+
* @param details - The agent details to backfill the agentId for
|
|
669
|
+
* @returns The agent details with the agentId backfilled
|
|
670
|
+
*/
|
|
671
|
+
backfillAgentDetailsId(details) {
|
|
672
|
+
if (!details)
|
|
673
|
+
return;
|
|
674
|
+
const knownId = this.resolvedAgentId ?? this.config.id;
|
|
675
|
+
if (knownId == null)
|
|
676
|
+
return;
|
|
677
|
+
if (details.agentId == null || details.agentId === '') {
|
|
678
|
+
details.agentId = knownId;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Get the agent details
|
|
683
|
+
* Returns cached agent details if available, otherwise fetches from network.
|
|
684
|
+
* If called before initialize(), performs minimal initialization to fetch agent details only
|
|
685
|
+
* (gets deployment info, fetches anonymous token, fetches agent details - without getting session ID or creating connection).
|
|
686
|
+
* @returns Promise resolving to the agent details
|
|
687
|
+
*/
|
|
688
|
+
async getAgentDetails() {
|
|
689
|
+
if (this.agentDetails) {
|
|
690
|
+
return this.agentDetails;
|
|
691
|
+
}
|
|
692
|
+
// If SDK is fully initialized, just fetch with existing token
|
|
693
|
+
if (this.isInitialized) {
|
|
694
|
+
const accessToken = await this.authService.getToken();
|
|
695
|
+
return await this.fetchAgentDetails(accessToken);
|
|
696
|
+
}
|
|
697
|
+
// Minimal initialization: only what's needed for agent details
|
|
698
|
+
this.logger.debug('Performing minimal initialization to fetch agent details', { agentId: this.config.id });
|
|
699
|
+
// 1. Get deployment info (uses cache if available)
|
|
700
|
+
const deploymentInfo = await this.getDeploymentInfo();
|
|
701
|
+
if (!deploymentInfo) {
|
|
702
|
+
const error = new Error('Deployment information not found');
|
|
703
|
+
this.logger.error('Failed to get agent details: deployment info not found', error, { agentId: this.config.id });
|
|
704
|
+
throw error;
|
|
705
|
+
}
|
|
706
|
+
// 2. Ensure apiHelper is set up
|
|
707
|
+
if (!this.apiHelper) {
|
|
708
|
+
this.apiHelper = new ApiHelper({
|
|
709
|
+
apiDomain: deploymentInfo.apiDomain,
|
|
710
|
+
cache: this.config.cache,
|
|
711
|
+
getToken: () => this.authService.getToken(),
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
// 3. Get token from the main strategy (defaults to anonymous if no auth config provided)
|
|
715
|
+
// Initialize auth service if not already initialized
|
|
716
|
+
if (!this.authService.getIsInitialized()) {
|
|
717
|
+
await this.authService.initialize({
|
|
718
|
+
deploymentInfo: deploymentInfo,
|
|
719
|
+
// No postAuthentication - we don't want session ID or connection yet
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
const accessToken = await this.authService.getToken();
|
|
723
|
+
return await this.fetchAgentDetails(accessToken);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Get the deployment information
|
|
727
|
+
* Returns cached deployment info if available, otherwise fetches from network.
|
|
728
|
+
* Does not require initialization - only needs the endpoint URL.
|
|
729
|
+
* @returns Promise resolving to the deployment information
|
|
730
|
+
*/
|
|
731
|
+
async getDeploymentInfo() {
|
|
732
|
+
if (this.deploymentInfo) {
|
|
733
|
+
return this.deploymentInfo;
|
|
734
|
+
}
|
|
735
|
+
// Fetch from network if not cached
|
|
736
|
+
this.logger.debug('Fetching deployment info from network', { endpoint: this.config.endpoint });
|
|
737
|
+
this.deploymentInfo = await ApiHelper.getDeploymentInfo(this.config.endpoint);
|
|
738
|
+
return this.deploymentInfo;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Get the agent name from cached agent details
|
|
742
|
+
* @returns The agent name
|
|
743
|
+
*/
|
|
744
|
+
async getAgentName() {
|
|
745
|
+
if (!this.agentDetails) {
|
|
746
|
+
throw new Error('Agent details not found. Call initialize() first.');
|
|
747
|
+
}
|
|
748
|
+
return this.agentDetails?.name ?? this.agentDetails?.agentProfileDetails?.name ?? '';
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Get session ID
|
|
752
|
+
* @param accessToken - The access token to use for authentication
|
|
753
|
+
* @returns The session ID
|
|
754
|
+
*/
|
|
755
|
+
async getSessionId(accessToken) {
|
|
756
|
+
// If sessionId was provided in config, return it (skip network fetch)
|
|
757
|
+
if (this.sessionId !== undefined) {
|
|
758
|
+
this.logger.debug('Using sessionId from config, skipping network fetch', {
|
|
759
|
+
sessionId: this.sessionId,
|
|
760
|
+
agentId: this.resolvedAgentId,
|
|
761
|
+
});
|
|
762
|
+
return this.sessionId;
|
|
763
|
+
}
|
|
764
|
+
// Otherwise, fetch from network (uses resolvedAgentId for chat identity)
|
|
765
|
+
return await this.apiHelper?.getAiAgentSession({
|
|
766
|
+
agentId: this.resolvedAgentId,
|
|
767
|
+
authToken: accessToken ?? await this.authService.getToken() ?? null,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Get WebSocket endpoint
|
|
772
|
+
* @param sessionId - The session ID
|
|
773
|
+
* @returns The WebSocket endpoint
|
|
774
|
+
*/
|
|
775
|
+
getWsEndpoint(sessionId) {
|
|
776
|
+
let websocketUrl = this.deploymentInfo?.aiAgentDomain;
|
|
777
|
+
websocketUrl = websocketUrl.indexOf("http") !== 0 ? "https://" + websocketUrl : websocketUrl;
|
|
778
|
+
try {
|
|
779
|
+
const parsedUrl = new URL(websocketUrl);
|
|
780
|
+
parsedUrl.hostname = `chat.${parsedUrl.hostname}`;
|
|
781
|
+
websocketUrl = parsedUrl.toString();
|
|
782
|
+
websocketUrl = `${websocketUrl}?sessionId=${sessionId}`;
|
|
783
|
+
this.logger.debug('WebSocket endpoint constructed', { endpoint: websocketUrl, sessionId });
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
const err = new Error('Failed to get WebSocket endpoint');
|
|
787
|
+
this.logger.error('Failed to construct WebSocket endpoint', err, { sessionId });
|
|
788
|
+
throw err;
|
|
789
|
+
}
|
|
790
|
+
return websocketUrl;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Create the connection after authentication is complete
|
|
794
|
+
* This is called by the postAuthentication callback
|
|
795
|
+
*/
|
|
796
|
+
async createConnection(sessionId) {
|
|
797
|
+
if (this.connection) {
|
|
798
|
+
this.logger.debug('Connection already exists, skipping creation', { sessionId });
|
|
799
|
+
return; // Connection already exists
|
|
800
|
+
}
|
|
801
|
+
const wsEndpoint = this.getWsEndpoint(sessionId);
|
|
802
|
+
this.logger.info('Creating connection', { endpoint: wsEndpoint, sessionId });
|
|
803
|
+
// Create connection
|
|
804
|
+
this.connection = new Connection({
|
|
805
|
+
endpoint: wsEndpoint,
|
|
806
|
+
maxReconnectAttempts: this.config.maxReconnectAttempts,
|
|
807
|
+
baseReconnectDelay: this.config.baseReconnectDelay,
|
|
808
|
+
maxReconnectDelay: this.config.maxReconnectDelay,
|
|
809
|
+
logger: this.logger.createChild('Connection'),
|
|
810
|
+
});
|
|
811
|
+
// Forward connection events
|
|
812
|
+
this.setupConnectionEvents();
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Whether the agent has completed initialization.
|
|
816
|
+
* Becomes `true` after the init pipeline completes (e.g. after the `initialized` event).
|
|
817
|
+
*
|
|
818
|
+
* @returns `true` if initialized, `false` otherwise
|
|
819
|
+
*
|
|
820
|
+
* @example
|
|
821
|
+
* ```typescript
|
|
822
|
+
* if (agent.getIsInitialized()) {
|
|
823
|
+
* await agent.connect();
|
|
824
|
+
* } else {
|
|
825
|
+
* agent.once('initialized', () => agent.connect());
|
|
826
|
+
* }
|
|
827
|
+
* ```
|
|
828
|
+
*/
|
|
829
|
+
getIsInitialized() {
|
|
830
|
+
return this.isInitialized;
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Get current connection state.
|
|
834
|
+
*
|
|
835
|
+
* @returns The current connection state
|
|
836
|
+
* @throws Error if agent is not initialized
|
|
837
|
+
*
|
|
838
|
+
* @example
|
|
839
|
+
* ```typescript
|
|
840
|
+
* const state = agent.getState();
|
|
841
|
+
* if (state === ConnectionState.CONNECTED) {
|
|
842
|
+
* console.log("Ready to send messages");
|
|
843
|
+
* }
|
|
844
|
+
* ```
|
|
845
|
+
*
|
|
846
|
+
* @see {@link ConnectionState} for available states
|
|
847
|
+
*/
|
|
848
|
+
getState() {
|
|
849
|
+
if (!this.connection) {
|
|
850
|
+
const error = new Error('Connection not initialized. Call initialize() first.');
|
|
851
|
+
this.logger.error('Failed to get connection state', error, { agentId: this.resolvedAgentId });
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
854
|
+
return this.connection.getState();
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Check if the agent is currently connected.
|
|
858
|
+
*
|
|
859
|
+
* @returns `true` if connected, `false` otherwise
|
|
860
|
+
*
|
|
861
|
+
* @example
|
|
862
|
+
* ```typescript
|
|
863
|
+
* if (agent.isConnected()) {
|
|
864
|
+
* await agent.send("Hello!");
|
|
865
|
+
* } else {
|
|
866
|
+
* console.log("Waiting for connection...");
|
|
867
|
+
* }
|
|
868
|
+
* ```
|
|
869
|
+
*/
|
|
870
|
+
isConnected() {
|
|
871
|
+
return this.connection?.isConnected() ?? false;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Connect to the agent endpoint.
|
|
875
|
+
*
|
|
876
|
+
* Establishes a WebSocket connection to the AI Agent server. Must call {@link initialize} first.
|
|
877
|
+
*
|
|
878
|
+
* For Contact Center agents, the connection object is created lazily on this call (session fetch
|
|
879
|
+
* uses the resolved agent ID, including Flow B after agent selection).
|
|
880
|
+
*
|
|
881
|
+
* @throws Error if agent is not initialized
|
|
882
|
+
*
|
|
883
|
+
* @example
|
|
884
|
+
* ```typescript
|
|
885
|
+
* await agent.initialize();
|
|
886
|
+
* await agent.connect();
|
|
887
|
+
* console.log("Connected!");
|
|
888
|
+
* ```
|
|
889
|
+
*/
|
|
890
|
+
async connect() {
|
|
891
|
+
this.logger.debug('connect() called', {
|
|
892
|
+
agentId: this.resolvedAgentId,
|
|
893
|
+
instanceId: this._instanceId,
|
|
894
|
+
isInitialized: this.isInitialized,
|
|
895
|
+
hasConnection: !!this.connection,
|
|
896
|
+
});
|
|
897
|
+
if (!this.isInitialized) {
|
|
898
|
+
const error = new Error('Agent not initialized. Call initialize() first.');
|
|
899
|
+
this.logger.error('Failed to connect: agent not initialized', error, {
|
|
900
|
+
agentId: this.resolvedAgentId,
|
|
901
|
+
isInitialized: this.isInitialized,
|
|
902
|
+
});
|
|
903
|
+
throw error;
|
|
904
|
+
}
|
|
905
|
+
// Lazy connection creation: if no connection exists (CC flow or PKCE redirect),
|
|
906
|
+
// get token, fetch session, create connection
|
|
907
|
+
if (!this.connection) {
|
|
908
|
+
const accessToken = await this.authService.getToken();
|
|
909
|
+
if (!accessToken) {
|
|
910
|
+
const error = new Error('Connection not initialized. Ensure initialize() completed successfully and authentication completed.');
|
|
911
|
+
this.logger.error('Failed to connect: no access token', error, {
|
|
912
|
+
agentId: this.resolvedAgentId,
|
|
913
|
+
isInitialized: this.isInitialized,
|
|
914
|
+
hasConnection: false,
|
|
915
|
+
});
|
|
916
|
+
throw error;
|
|
917
|
+
}
|
|
918
|
+
this.logger.debug('Lazily creating connection', { agentId: this.resolvedAgentId });
|
|
919
|
+
this.sessionId = await this.getSessionId(accessToken);
|
|
920
|
+
await this.createConnection(this.sessionId);
|
|
921
|
+
}
|
|
922
|
+
this.logger.info('Connecting to agent', { agentId: this.resolvedAgentId });
|
|
923
|
+
await this.connection.connect();
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Disconnect from the agent endpoint.
|
|
927
|
+
*
|
|
928
|
+
* By default, sends a graceful disconnect message before closing the connection.
|
|
929
|
+
* Use `skipGracefulDisconnect: true` for immediate disconnection.
|
|
930
|
+
*
|
|
931
|
+
* @param options - Disconnect options
|
|
932
|
+
* @param options.skipGracefulDisconnect - If true, skip sending graceful disconnect message
|
|
933
|
+
*
|
|
934
|
+
* @example Graceful disconnect
|
|
935
|
+
* ```typescript
|
|
936
|
+
* await agent.disconnect();
|
|
937
|
+
* ```
|
|
938
|
+
*
|
|
939
|
+
* @example Immediate disconnect
|
|
940
|
+
* ```typescript
|
|
941
|
+
* await agent.disconnect({ skipGracefulDisconnect: true });
|
|
942
|
+
* ```
|
|
943
|
+
*/
|
|
944
|
+
async disconnect(options) {
|
|
945
|
+
if (!this.connection) {
|
|
946
|
+
this.logger.debug('Connection not initialized, skipping disconnect', { agentId: this.resolvedAgentId });
|
|
947
|
+
return; // Already disconnected or not initialized
|
|
948
|
+
}
|
|
949
|
+
// Send graceful disconnect message if connected and not skipped
|
|
950
|
+
if (this.connection.isConnected() && !options?.skipGracefulDisconnect) {
|
|
951
|
+
try {
|
|
952
|
+
this.logger.debug('Sending graceful disconnect message', { agentId: this.resolvedAgentId });
|
|
953
|
+
const gracefulDisconnectMessage = createGracefulDisconnectMessage();
|
|
954
|
+
await this.send(gracefulDisconnectMessage);
|
|
955
|
+
// Give a small delay to ensure the message is sent before disconnecting
|
|
956
|
+
await this.delay(100);
|
|
957
|
+
}
|
|
958
|
+
catch (error) {
|
|
959
|
+
// Log error but continue with disconnect
|
|
960
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
961
|
+
this.logger.warn('Failed to send graceful disconnect message, continuing with disconnect', {
|
|
962
|
+
error: err,
|
|
963
|
+
agentId: this.resolvedAgentId
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
this.logger.info('Disconnecting from agent', { agentId: this.resolvedAgentId });
|
|
968
|
+
this.connection.disconnect();
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Select a portal (CC flow). Call when portalsAvailable event is emitted and user has chosen.
|
|
972
|
+
*
|
|
973
|
+
* @param portal - The selected portal
|
|
974
|
+
* @throws Error if portal initializer is not active
|
|
975
|
+
*
|
|
976
|
+
* @example
|
|
977
|
+
* ```typescript
|
|
978
|
+
* agent.on('portalsAvailable', (e) => {
|
|
979
|
+
* const portal = showPortalPicker(e.payload.portals);
|
|
980
|
+
* agent.selectPortal(portal);
|
|
981
|
+
* });
|
|
982
|
+
* ```
|
|
983
|
+
*/
|
|
984
|
+
selectPortal(portal) {
|
|
985
|
+
if (!this.portalInitializer) {
|
|
986
|
+
throw new Error('selectPortal can only be called during portal initialization flow');
|
|
987
|
+
}
|
|
988
|
+
this.portalInitializer.onPortalSelected(portal);
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Select an agent (Flow B only — `initParams.isDefaultAgent === "true"`).
|
|
992
|
+
* Call when `agentsAvailable` is emitted and the user has chosen.
|
|
993
|
+
*
|
|
994
|
+
* @param agent - The selected agent
|
|
995
|
+
* @throws Error if portal initializer is not active
|
|
996
|
+
*
|
|
997
|
+
* @example
|
|
998
|
+
* ```typescript
|
|
999
|
+
* agent.on('agentsAvailable', (e) => {
|
|
1000
|
+
* const selected = showAgentPicker(e.payload.agents);
|
|
1001
|
+
* agent.selectAgent(selected);
|
|
1002
|
+
* });
|
|
1003
|
+
* ```
|
|
1004
|
+
*/
|
|
1005
|
+
selectAgent(agent) {
|
|
1006
|
+
if (!this.portalInitializer) {
|
|
1007
|
+
throw new Error('selectAgent can only be called during portal initialization flow');
|
|
1008
|
+
}
|
|
1009
|
+
this.portalInitializer.onAgentSelected(agent);
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Select a user profile (CC flow). Call when profilesAvailable event is emitted and user has chosen.
|
|
1013
|
+
*
|
|
1014
|
+
* @param profile - The selected profile
|
|
1015
|
+
* @throws Error if portal initializer is not active
|
|
1016
|
+
*
|
|
1017
|
+
* @example
|
|
1018
|
+
* ```typescript
|
|
1019
|
+
* agent.on('profilesAvailable', (e) => {
|
|
1020
|
+
* const profile = showProfilePicker(e.payload.profiles);
|
|
1021
|
+
* agent.selectUserProfile(profile);
|
|
1022
|
+
* });
|
|
1023
|
+
* ```
|
|
1024
|
+
*/
|
|
1025
|
+
selectUserProfile(profile) {
|
|
1026
|
+
if (!this.portalInitializer) {
|
|
1027
|
+
throw new Error('selectUserProfile can only be called during portal initialization flow');
|
|
1028
|
+
}
|
|
1029
|
+
this.portalInitializer.onProfileSelected(profile);
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Get the stored initialization parameters from config.
|
|
1033
|
+
*
|
|
1034
|
+
* @returns The init params object (empty object if none provided)
|
|
1035
|
+
*
|
|
1036
|
+
* @example
|
|
1037
|
+
* ```typescript
|
|
1038
|
+
* const initParams = agent.getInitParams();
|
|
1039
|
+
* const userId = initParams.userid;
|
|
1040
|
+
* ```
|
|
1041
|
+
*/
|
|
1042
|
+
getInitParams() {
|
|
1043
|
+
return { ...this.initParams };
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Restart the CC widget initialization pipeline from scratch.
|
|
1047
|
+
*
|
|
1048
|
+
* This method tears down the current initialization state and re-runs the
|
|
1049
|
+
* full pipeline (portal selection → agent selection → profile selection),
|
|
1050
|
+
* allowing the consumer to make new selections. After completion, the
|
|
1051
|
+
* `initialized` event fires again and the consumer should call `connect()`.
|
|
1052
|
+
*
|
|
1053
|
+
* **What it does:**
|
|
1054
|
+
* 1. Checks `completedPortalPipeline`: if false (non-CC agent), delegates to `restartConnection()` and returns
|
|
1055
|
+
* 2. Destroys the current `PortalInitializer` instance (rejects any pending gating promises)
|
|
1056
|
+
* 3. Disconnects the current WebSocket connection (if any) and clears session, queue, transcript
|
|
1057
|
+
* 4. Resets `resolvedAgentId` to `config.id` and `isInitialized` to false
|
|
1058
|
+
* 5. Re-obtains an auth token and calls `onAuthComplete` to restart the pipeline (or direct flow)
|
|
1059
|
+
*
|
|
1060
|
+
* **Important:** For agents that completed the CC initialization pipeline
|
|
1061
|
+
* (portal → agent → profile selection), this re-runs the full pipeline.
|
|
1062
|
+
* For agents that did not complete it (e.g. direct flow from the start, or
|
|
1063
|
+
* contact-center agents that fell back to direct flow because they have no
|
|
1064
|
+
* portals), this method delegates to `restartConnection()` so the consumer
|
|
1065
|
+
* can call it for any restart without branching. The consumer must
|
|
1066
|
+
* re-register or still have active event listeners for `portalsAvailable`,
|
|
1067
|
+
* `agentsAvailable`, `profilesAvailable`, and `initialized` before calling
|
|
1068
|
+
* this method (CC pipeline path only).
|
|
1069
|
+
*
|
|
1070
|
+
* @throws Error if authentication token cannot be obtained (CC path) or for restart (direct path)
|
|
1071
|
+
*
|
|
1072
|
+
* @example
|
|
1073
|
+
* ```typescript
|
|
1074
|
+
* // User wants to switch portals — restart the pipeline
|
|
1075
|
+
* agent.on('portalsAvailable', (e) => {
|
|
1076
|
+
* showPortalPicker(e.payload.portals, (p) => agent.selectPortal(p));
|
|
1077
|
+
* });
|
|
1078
|
+
*
|
|
1079
|
+
* agent.on('initialized', async () => {
|
|
1080
|
+
* await agent.connect();
|
|
1081
|
+
* });
|
|
1082
|
+
*
|
|
1083
|
+
* await agent.restartPortalInitializer();
|
|
1084
|
+
* ```
|
|
1085
|
+
*/
|
|
1086
|
+
async restartPortalInitializer() {
|
|
1087
|
+
if (!this.completedPortalPipeline) {
|
|
1088
|
+
this.logger.debug('restartPortalInitializer: agent did not complete portal pipeline, delegating to restartConnection', {
|
|
1089
|
+
agentId: this.config.id,
|
|
1090
|
+
});
|
|
1091
|
+
await this.restartConnection();
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (this.portalInitializer) {
|
|
1095
|
+
this.portalInitializer.destroy();
|
|
1096
|
+
this.portalInitializer = undefined;
|
|
1097
|
+
}
|
|
1098
|
+
// Clear profile cache so the restarted pipeline fetches fresh data
|
|
1099
|
+
if (this.lastSelectedPortal?.id != null) {
|
|
1100
|
+
this.contextCacheAdapter.delete(this.getPipelineProfilesCacheKey(this.lastSelectedPortal.id));
|
|
1101
|
+
}
|
|
1102
|
+
if (this.connection) {
|
|
1103
|
+
await this.disconnect();
|
|
1104
|
+
}
|
|
1105
|
+
this.connection = undefined;
|
|
1106
|
+
this.sessionId = undefined;
|
|
1107
|
+
this.clearQueue();
|
|
1108
|
+
this.clearTranscript();
|
|
1109
|
+
if (this.initParams.platform === "test") {
|
|
1110
|
+
this.clearCallTranscript();
|
|
1111
|
+
}
|
|
1112
|
+
this.resolvedAgentId = this.config.id;
|
|
1113
|
+
this.isInitialized = false;
|
|
1114
|
+
const accessToken = await this.authService.getToken();
|
|
1115
|
+
if (!accessToken) {
|
|
1116
|
+
const error = new Error('Failed to get access token for restart');
|
|
1117
|
+
this.logger.error('restartPortalInitializer: no access token', error, { agentId: this.config.id });
|
|
1118
|
+
throw error;
|
|
1119
|
+
}
|
|
1120
|
+
await this.onAuthComplete(accessToken);
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* @deprecated Use {@link restartPortalInitializer} instead.
|
|
1124
|
+
*/
|
|
1125
|
+
async restartCcWidgetInitializer() {
|
|
1126
|
+
return this.restartPortalInitializer();
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Update the active user profile after initialization.
|
|
1130
|
+
*
|
|
1131
|
+
* Use this when the consumer wants to switch profiles without re-running the
|
|
1132
|
+
* full portal/agent selection pipeline. This method:
|
|
1133
|
+
* 1. Persists the new profile selection via the `selectUserProfile` API
|
|
1134
|
+
* (if the profile is not already the last-used profile)
|
|
1135
|
+
* 2. Disconnects the current WebSocket connection (if any)
|
|
1136
|
+
* 3. Clears the message queue and transcript
|
|
1137
|
+
* 4. Fetches a new session ID and reconnects
|
|
1138
|
+
* 5. Emits `initialized` with the updated profile in the payload
|
|
1139
|
+
*
|
|
1140
|
+
* This is the equivalent of the cc-widget's profile dropdown behavior:
|
|
1141
|
+
* change the profile → restart session → re-send user context.
|
|
1142
|
+
*
|
|
1143
|
+
* **Important:** This method is only valid after the CC initialization
|
|
1144
|
+
* pipeline has completed. Calling it on a non-CC agent or before
|
|
1145
|
+
* initialization throws an error.
|
|
1146
|
+
*
|
|
1147
|
+
* @param profile - The new user profile to activate
|
|
1148
|
+
* @throws Error if agent is not initialized
|
|
1149
|
+
* @throws Error if agent did not go through the CC initialization flow
|
|
1150
|
+
* @throws Error if no portal is currently selected
|
|
1151
|
+
*
|
|
1152
|
+
* @example
|
|
1153
|
+
* ```typescript
|
|
1154
|
+
* // User picks a different profile from a dropdown
|
|
1155
|
+
* const profiles = cachedProfiles; // from the profilesAvailable event
|
|
1156
|
+
* agent.on("initialized", async () => {
|
|
1157
|
+
* await agent.connect();
|
|
1158
|
+
* });
|
|
1159
|
+
* await agent.updateUserProfile(profiles[2]);
|
|
1160
|
+
* ```
|
|
1161
|
+
*/
|
|
1162
|
+
async updateUserProfile(profile) {
|
|
1163
|
+
if (!this.isInitialized) {
|
|
1164
|
+
const error = new Error('updateUserProfile can only be called after initialization');
|
|
1165
|
+
this.logger.error('updateUserProfile: agent not initialized', error, { agentId: this.config.id });
|
|
1166
|
+
throw error;
|
|
1167
|
+
}
|
|
1168
|
+
if (!this.completedPortalPipeline) {
|
|
1169
|
+
const error = new Error('updateUserProfile can only be called on agents that used the portal initialization flow');
|
|
1170
|
+
this.logger.error('updateUserProfile: not a portal-configured agent', error, { agentId: this.config.id });
|
|
1171
|
+
throw error;
|
|
1172
|
+
}
|
|
1173
|
+
if (!this.lastSelectedPortal) {
|
|
1174
|
+
const error = new Error('updateUserProfile requires a selected portal');
|
|
1175
|
+
this.logger.error('updateUserProfile: no portal selected', error, { agentId: this.config.id });
|
|
1176
|
+
throw error;
|
|
1177
|
+
}
|
|
1178
|
+
if (profile.id !== 'none' && !profile.isLastUsedInPortal && this.apiHelper) {
|
|
1179
|
+
try {
|
|
1180
|
+
await this.apiHelper.selectUserProfile({
|
|
1181
|
+
portalId: this.lastSelectedPortal.id,
|
|
1182
|
+
profileId: profile.id,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
catch (err) {
|
|
1186
|
+
this.logger.warn('Failed to persist profile selection in updateUserProfile', {
|
|
1187
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1188
|
+
agentId: this.config.id,
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
await this.restartConnection();
|
|
1193
|
+
const payload = {
|
|
1194
|
+
portal: this.lastSelectedPortal,
|
|
1195
|
+
profile,
|
|
1196
|
+
availableProfiles: this.cachedProfiles,
|
|
1197
|
+
agent: this.agentDetails ?? { agentId: this.resolvedAgentId, name: 'Unknown' },
|
|
1198
|
+
};
|
|
1199
|
+
this.emit('initialized', this.createAgentEventResponse('initialized', payload));
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Restart the connection with a fresh session.
|
|
1203
|
+
*
|
|
1204
|
+
* This method:
|
|
1205
|
+
* 1. Gracefully disconnects from the current session
|
|
1206
|
+
* 2. Clears all queued messages and transcript
|
|
1207
|
+
* 3. Obtains a new session ID (or uses provided one)
|
|
1208
|
+
* 4. Reconnects to the new session
|
|
1209
|
+
* 5. Sends any stored context to the new session
|
|
1210
|
+
*
|
|
1211
|
+
* **Note:** All queued messages will be lost during restart.
|
|
1212
|
+
*
|
|
1213
|
+
* @param options - Optional restart options
|
|
1214
|
+
* @param options.sessionId - Optional session ID to use for restart. If provided, skips fetching from network.
|
|
1215
|
+
* @throws Error if agent is not initialized
|
|
1216
|
+
*
|
|
1217
|
+
* @example
|
|
1218
|
+
* ```typescript
|
|
1219
|
+
* // Start a fresh conversation (fetches new sessionId)
|
|
1220
|
+
* await agent.restartConnection();
|
|
1221
|
+
*
|
|
1222
|
+
* // Restart with a specific sessionId
|
|
1223
|
+
* await agent.restartConnection({ sessionId: 'existing-session-id' });
|
|
1224
|
+
*
|
|
1225
|
+
* // Context is automatically restored
|
|
1226
|
+
* await agent.send("Hello again!");
|
|
1227
|
+
* ```
|
|
1228
|
+
*/
|
|
1229
|
+
async restartConnection(options) {
|
|
1230
|
+
// Validate that agent is initialized
|
|
1231
|
+
if (!this.isInitialized) {
|
|
1232
|
+
const error = new Error('Agent not initialized. Call initialize() first.');
|
|
1233
|
+
this.logger.error('Failed to restart connection: agent not initialized', error, { agentId: this.resolvedAgentId });
|
|
1234
|
+
throw error;
|
|
1235
|
+
}
|
|
1236
|
+
if (!this.apiHelper) {
|
|
1237
|
+
const error = new Error('API helper not initialized. Call initialize() first.');
|
|
1238
|
+
this.logger.error('Failed to restart connection: API helper not initialized', error, { agentId: this.resolvedAgentId });
|
|
1239
|
+
throw error;
|
|
1240
|
+
}
|
|
1241
|
+
try {
|
|
1242
|
+
this.logger.info('Restarting connection', { agentId: this.resolvedAgentId, sessionId: this.sessionId });
|
|
1243
|
+
// Step 1: Gracefully disconnect from current session
|
|
1244
|
+
if (this.connection) {
|
|
1245
|
+
await this.disconnect();
|
|
1246
|
+
}
|
|
1247
|
+
// Step 2: Clear all queued messages and transcript - previous messages will be lost
|
|
1248
|
+
this.clearQueue();
|
|
1249
|
+
this.clearTranscript();
|
|
1250
|
+
this.logger.debug('Message queue and transcript cleared', { agentId: this.resolvedAgentId });
|
|
1251
|
+
// Step 3: Get new sessionId
|
|
1252
|
+
let newSessionId;
|
|
1253
|
+
if (options?.sessionId !== undefined) {
|
|
1254
|
+
// Use provided sessionId
|
|
1255
|
+
newSessionId = options.sessionId;
|
|
1256
|
+
this.logger.debug('Using provided sessionId for restart', {
|
|
1257
|
+
agentId: this.resolvedAgentId,
|
|
1258
|
+
providedSessionId: newSessionId
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
else {
|
|
1262
|
+
// Fetch new sessionId from API (ignore config sessionId for restart)
|
|
1263
|
+
const accessToken = await this.authService.getToken();
|
|
1264
|
+
if (!accessToken) {
|
|
1265
|
+
const error = new Error('Failed to get access token for restart');
|
|
1266
|
+
this.logger.error('Failed to restart connection: access token not available', error, { agentId: this.resolvedAgentId });
|
|
1267
|
+
throw error;
|
|
1268
|
+
}
|
|
1269
|
+
newSessionId = await this.apiHelper?.getAiAgentSession({
|
|
1270
|
+
agentId: this.resolvedAgentId,
|
|
1271
|
+
authToken: accessToken,
|
|
1272
|
+
});
|
|
1273
|
+
if (!newSessionId) {
|
|
1274
|
+
const error = new Error('Failed to get new sessionId for restart');
|
|
1275
|
+
this.logger.error('Failed to restart connection: new sessionId not obtained', error, { agentId: this.resolvedAgentId });
|
|
1276
|
+
throw error;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
this.logger.debug('New sessionId obtained', {
|
|
1280
|
+
agentId: this.resolvedAgentId,
|
|
1281
|
+
oldSessionId: this.sessionId,
|
|
1282
|
+
newSessionId
|
|
1283
|
+
});
|
|
1284
|
+
// Step 5: Clean up existing connection
|
|
1285
|
+
// Set connection to undefined to allow createConnection to create a new one
|
|
1286
|
+
this.connection = undefined;
|
|
1287
|
+
// Step 6: Update sessionId
|
|
1288
|
+
this.sessionId = newSessionId;
|
|
1289
|
+
// Step 7: Create new connection with new sessionId
|
|
1290
|
+
await this.createConnection(String(newSessionId));
|
|
1291
|
+
// Step 8: Connect to the new session
|
|
1292
|
+
await this.connect();
|
|
1293
|
+
// Step 9: Send stored context immediately after reconnection
|
|
1294
|
+
await this.sendStoredContext();
|
|
1295
|
+
this.logger.info('Connection restarted successfully', {
|
|
1296
|
+
agentId: this.resolvedAgentId,
|
|
1297
|
+
newSessionId
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
catch (error) {
|
|
1301
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1302
|
+
this.logger.error('Failed to restart connection', err, { agentId: this.resolvedAgentId });
|
|
1303
|
+
this.emit('error', this.createAgentEventResponse('error', {
|
|
1304
|
+
error: err,
|
|
1305
|
+
}));
|
|
1306
|
+
throw err;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Normalize input data to a Message instance
|
|
1311
|
+
* @private
|
|
1312
|
+
*/
|
|
1313
|
+
normalizeToMessage(data, options) {
|
|
1314
|
+
// Already a Message instance
|
|
1315
|
+
if (data instanceof Message) {
|
|
1316
|
+
// Update from/to if provided in options
|
|
1317
|
+
if (options?.from !== undefined || options?.to !== undefined) {
|
|
1318
|
+
return data.clone({
|
|
1319
|
+
from: options?.from ?? data.from,
|
|
1320
|
+
to: options?.to ?? data.to,
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
return data;
|
|
1324
|
+
}
|
|
1325
|
+
else if (typeof data === 'string') {
|
|
1326
|
+
// String input - create customer message
|
|
1327
|
+
return new Message(PERSONA.CUSTOMER, ROLE.HUMAN, data, {
|
|
1328
|
+
messageId: options?.id,
|
|
1329
|
+
from: options?.from,
|
|
1330
|
+
to: options?.to,
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
else if (data && typeof data === 'object') {
|
|
1334
|
+
// Object input - extract message properties
|
|
1335
|
+
return new Message(data.persona || PERSONA.CUSTOMER, data.role || ROLE.HUMAN, data.content, {
|
|
1336
|
+
messageId: options?.id || data.messageId,
|
|
1337
|
+
messageData: data.messageData,
|
|
1338
|
+
from: options?.from || data.from,
|
|
1339
|
+
to: options?.to || data.to,
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
else {
|
|
1343
|
+
throw new MessageError('Invalid message data: must be a Message instance, string, or object');
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Send a message to the agent
|
|
1348
|
+
* Messages are queued if offline and automatically sent when connected
|
|
1349
|
+
* Context messages are automatically stored in cache for use on reconnection
|
|
1350
|
+
* @param data - Message data (can be a Message instance, plain object, or string)
|
|
1351
|
+
* @param options - Optional message options
|
|
1352
|
+
* @param options.id - Optional message ID
|
|
1353
|
+
* @param options.from - Optional sender identifier (agent ID, customer ID, etc.)
|
|
1354
|
+
* @param options.to - Optional recipient identifier (agent ID, etc.)
|
|
1355
|
+
* @returns Message ID
|
|
1356
|
+
*/
|
|
1357
|
+
async send(data, options) {
|
|
1358
|
+
// Check if this is a context message and store the context before normalizing
|
|
1359
|
+
if (this.isContextMessage(data)) {
|
|
1360
|
+
const context = this.extractContextFromMessage(data);
|
|
1361
|
+
if (context) {
|
|
1362
|
+
this.storeContext(context);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
let message = this.normalizeToMessage(data, options);
|
|
1366
|
+
// Assign default from/to values before validation
|
|
1367
|
+
if (!message.from || !message.to) {
|
|
1368
|
+
message = message.clone({
|
|
1369
|
+
from: message.from || 'customer',
|
|
1370
|
+
to: message.to || await this.getAgentName(),
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
// Validate message
|
|
1374
|
+
message.validate();
|
|
1375
|
+
// Serialize message for transmission
|
|
1376
|
+
const payload = message.toPayloadString();
|
|
1377
|
+
this.logger.debug('Sending message', {
|
|
1378
|
+
messageId: message.messageId,
|
|
1379
|
+
from: message.from,
|
|
1380
|
+
to: message.to,
|
|
1381
|
+
payload
|
|
1382
|
+
});
|
|
1383
|
+
const messageId = message.messageId || options?.id || this.generateMessageId();
|
|
1384
|
+
// Store message in transcript before sending
|
|
1385
|
+
this.transcript.add(message, 'sent', this.sessionId, this.resolvedAgentId);
|
|
1386
|
+
// Emit transcript update event
|
|
1387
|
+
this.emit('transcriptUpdate', {
|
|
1388
|
+
type: 'transcriptUpdate',
|
|
1389
|
+
timestamp: Date.now(),
|
|
1390
|
+
sessionId: this.sessionId,
|
|
1391
|
+
agentId: this.resolvedAgentId,
|
|
1392
|
+
payload: {
|
|
1393
|
+
entry: {
|
|
1394
|
+
message,
|
|
1395
|
+
direction: 'sent',
|
|
1396
|
+
timestamp: message.timestamp || Date.now(),
|
|
1397
|
+
sessionId: this.sessionId,
|
|
1398
|
+
agentId: this.resolvedAgentId,
|
|
1399
|
+
},
|
|
1400
|
+
},
|
|
1401
|
+
});
|
|
1402
|
+
// If connection exists and is connected, try to send immediately
|
|
1403
|
+
if (this.connection?.isConnected()) {
|
|
1404
|
+
try {
|
|
1405
|
+
await this.connection.send(payload);
|
|
1406
|
+
this.logger.debug('Message sent successfully', { messageId });
|
|
1407
|
+
return messageId;
|
|
1408
|
+
}
|
|
1409
|
+
catch (error) {
|
|
1410
|
+
// If send fails, queue the message
|
|
1411
|
+
if (error instanceof ConnectionError) {
|
|
1412
|
+
this.logger.warn('Failed to send message, queueing', { messageId, error: error.message });
|
|
1413
|
+
return this.queueMessage(JSON.parse(payload), messageId);
|
|
1414
|
+
}
|
|
1415
|
+
this.logger.error('Failed to send message', error instanceof Error ? error : new Error(String(error)), { messageId });
|
|
1416
|
+
throw error;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
// Queue the message if not connected or connection not initialized
|
|
1420
|
+
this.logger.debug('Connection not available, queueing message', { messageId, queueSize: this.messageQueue.size() });
|
|
1421
|
+
return this.queueMessage(payload, messageId);
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Get the current queue size
|
|
1425
|
+
*/
|
|
1426
|
+
getQueueSize() {
|
|
1427
|
+
return this.messageQueue.size();
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Clear the message queue
|
|
1431
|
+
*/
|
|
1432
|
+
clearQueue() {
|
|
1433
|
+
this.messageQueue.clear();
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Get transcript entries
|
|
1437
|
+
* @param options - Optional filtering options
|
|
1438
|
+
* @returns Array of transcript entries with Message objects
|
|
1439
|
+
*/
|
|
1440
|
+
getTranscript(options) {
|
|
1441
|
+
return this.transcript.getEntries(options);
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Get transcript entries as plain objects (JSON-serializable)
|
|
1445
|
+
* @param options - Optional filtering options
|
|
1446
|
+
* @returns Array of plain objects representing transcript entries
|
|
1447
|
+
*/
|
|
1448
|
+
getTranscriptAsJSON(options) {
|
|
1449
|
+
return this.transcript.getEntriesAsJSON(options);
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Get the number of entries in the transcript
|
|
1453
|
+
* @returns Number of transcript entries
|
|
1454
|
+
*/
|
|
1455
|
+
getTranscriptSize() {
|
|
1456
|
+
return this.transcript.size();
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Clear all transcript entries
|
|
1460
|
+
*/
|
|
1461
|
+
clearTranscript() {
|
|
1462
|
+
this.transcript.clear();
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Get the call transcript — the live customer-agent conversation from the
|
|
1466
|
+
* telephony platform (Genesys, Amazon Connect, etc.), pushed by the platform
|
|
1467
|
+
* connector via the HookContract.
|
|
1468
|
+
*
|
|
1469
|
+
* This is distinct from {@link getTranscript}, which returns the AI Agent
|
|
1470
|
+
* chat transcript (WebSocket messages).
|
|
1471
|
+
*
|
|
1472
|
+
* @returns A shallow copy of the call transcript entries
|
|
1473
|
+
*/
|
|
1474
|
+
getCallTranscript() {
|
|
1475
|
+
return [...this.callTranscript];
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Get the caller information set by the platform connector via
|
|
1479
|
+
* HookContract.setCallerInfo(). Available after the platform connector
|
|
1480
|
+
* has initialized (typically before the `initialized` event fires).
|
|
1481
|
+
*
|
|
1482
|
+
* @returns The caller info object, or null if not yet set
|
|
1483
|
+
*/
|
|
1484
|
+
getCallerInfo() {
|
|
1485
|
+
return this.callerInfo;
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Returns the authenticated user's or customer's details fetched after authentication.
|
|
1489
|
+
* Available after the `initialized` event fires. Returns null if details could not be fetched.
|
|
1490
|
+
*/
|
|
1491
|
+
getUserDetails() {
|
|
1492
|
+
return this.userDetails;
|
|
1493
|
+
}
|
|
1494
|
+
getConversationId() {
|
|
1495
|
+
return this.conversationId;
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Clear all call transcript entries.
|
|
1499
|
+
*/
|
|
1500
|
+
clearCallTranscript() {
|
|
1501
|
+
this.callTranscript = [];
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Get the context cache key for this agent
|
|
1505
|
+
* @returns The cache key for storing context
|
|
1506
|
+
*/
|
|
1507
|
+
getContextCacheKey() {
|
|
1508
|
+
return `${CONTEXT_CACHE_KEY_PREFIX}${this.resolvedAgentId}`;
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Cache key for persisted profile list (pipeline restart).
|
|
1512
|
+
* Includes portalId so switching portals doesn't serve stale profiles.
|
|
1513
|
+
*/
|
|
1514
|
+
getPipelineProfilesCacheKey(portalId) {
|
|
1515
|
+
const suffix = portalId != null
|
|
1516
|
+
? `${this.resolvedAgentId}_${portalId}`
|
|
1517
|
+
: this.resolvedAgentId;
|
|
1518
|
+
return `${PIPELINE_PROFILES_CACHE_KEY_PREFIX}${suffix}`;
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Check if a message is a context message
|
|
1522
|
+
* @param data - The message data to check
|
|
1523
|
+
* @returns True if this is a context message
|
|
1524
|
+
*/
|
|
1525
|
+
isContextMessage(data) {
|
|
1526
|
+
if (data instanceof Message) {
|
|
1527
|
+
return data.persona === PERSONA.SYSTEM && data.role === ROLE.CONTEXT;
|
|
1528
|
+
}
|
|
1529
|
+
if (data && typeof data === 'object') {
|
|
1530
|
+
return data.persona === PERSONA.SYSTEM && data.role === ROLE.CONTEXT;
|
|
1531
|
+
}
|
|
1532
|
+
return false;
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Extract context data from a context message
|
|
1536
|
+
* @param data - The message data to extract context from
|
|
1537
|
+
* @returns The context object or null if not a context message
|
|
1538
|
+
*/
|
|
1539
|
+
extractContextFromMessage(data) {
|
|
1540
|
+
if (data instanceof Message) {
|
|
1541
|
+
return data.messageData?.context || null;
|
|
1542
|
+
}
|
|
1543
|
+
if (data && typeof data === 'object') {
|
|
1544
|
+
return data.messageData?.context || null;
|
|
1545
|
+
}
|
|
1546
|
+
return null;
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Store context in cache
|
|
1550
|
+
* @param context - The context object to store
|
|
1551
|
+
*/
|
|
1552
|
+
storeContext(context) {
|
|
1553
|
+
const cacheKey = this.getContextCacheKey();
|
|
1554
|
+
this.contextCacheAdapter.set(cacheKey, {
|
|
1555
|
+
value: context,
|
|
1556
|
+
timestamp: Date.now(),
|
|
1557
|
+
});
|
|
1558
|
+
this.logger.debug('Context stored in cache', { agentId: this.resolvedAgentId, context });
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Get the stored context for this agent
|
|
1562
|
+
* Returns the context object that was previously sent via a context message
|
|
1563
|
+
* @returns The stored context object or null if no context is stored
|
|
1564
|
+
*/
|
|
1565
|
+
getContext() {
|
|
1566
|
+
const cacheKey = this.getContextCacheKey();
|
|
1567
|
+
const entry = this.contextCacheAdapter.get(cacheKey);
|
|
1568
|
+
if (!entry) {
|
|
1569
|
+
return null;
|
|
1570
|
+
}
|
|
1571
|
+
return entry.value;
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Remove the stored context for this agent
|
|
1575
|
+
* Clears any previously stored context from the cache
|
|
1576
|
+
*/
|
|
1577
|
+
removeContext() {
|
|
1578
|
+
const cacheKey = this.getContextCacheKey();
|
|
1579
|
+
this.contextCacheAdapter.delete(cacheKey);
|
|
1580
|
+
this.logger.debug('Context removed from cache', { agentId: this.resolvedAgentId });
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Set context for this agent
|
|
1584
|
+
* Stores the context in cache and optionally sends it to the agent immediately
|
|
1585
|
+
* @param context - The context object to set
|
|
1586
|
+
* @param options - Optional settings
|
|
1587
|
+
* @param options.sendImmediately - If true, sends the context to the agent right away (default: false)
|
|
1588
|
+
* @returns Promise that resolves when context is set (and sent if sendImmediately is true)
|
|
1589
|
+
*
|
|
1590
|
+
* @example Set context without sending
|
|
1591
|
+
* ```typescript
|
|
1592
|
+
* agent.setContext({ userId: "123", plan: "premium" });
|
|
1593
|
+
* ```
|
|
1594
|
+
*
|
|
1595
|
+
* @example Set and send context immediately
|
|
1596
|
+
* ```typescript
|
|
1597
|
+
* await agent.setContext({ userId: "123", plan: "premium" }, { sendImmediately: true });
|
|
1598
|
+
* ```
|
|
1599
|
+
*/
|
|
1600
|
+
async setContext(context, options) {
|
|
1601
|
+
this.storeContext(context);
|
|
1602
|
+
this.logger.info('Context set', { agentId: this.resolvedAgentId, context });
|
|
1603
|
+
if (options?.sendImmediately) {
|
|
1604
|
+
const contextMessage = createContextMessage({ context });
|
|
1605
|
+
await this.send(contextMessage);
|
|
1606
|
+
this.logger.debug('Context sent immediately after setContext', { agentId: this.resolvedAgentId });
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Reset (clear) the context for this agent
|
|
1611
|
+
* Removes any stored context from the cache
|
|
1612
|
+
* This is an alias for removeContext() with additional logging
|
|
1613
|
+
*
|
|
1614
|
+
* @example
|
|
1615
|
+
* ```typescript
|
|
1616
|
+
* agent.resetContext();
|
|
1617
|
+
* ```
|
|
1618
|
+
*/
|
|
1619
|
+
resetContext() {
|
|
1620
|
+
this.removeContext();
|
|
1621
|
+
this.logger.info('Context reset', { agentId: this.resolvedAgentId });
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Send stored context to the agent
|
|
1625
|
+
* This is called internally after reconnection to restore context
|
|
1626
|
+
* @returns Promise that resolves when context is sent (or immediately if no context)
|
|
1627
|
+
*/
|
|
1628
|
+
async sendStoredContext() {
|
|
1629
|
+
const context = this.getContext();
|
|
1630
|
+
if (!context) {
|
|
1631
|
+
this.logger.debug('No stored context to send', { agentId: this.resolvedAgentId });
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
try {
|
|
1635
|
+
this.logger.debug('Sending stored context after reconnection', { agentId: this.resolvedAgentId, context });
|
|
1636
|
+
const contextMessage = createContextMessage({ context });
|
|
1637
|
+
await this.send(contextMessage);
|
|
1638
|
+
this.logger.info('Stored context sent successfully', { agentId: this.resolvedAgentId });
|
|
1639
|
+
}
|
|
1640
|
+
catch (error) {
|
|
1641
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1642
|
+
this.logger.error('Failed to send stored context', err, { agentId: this.resolvedAgentId });
|
|
1643
|
+
// Don't throw - allow the connection to continue even if context send fails
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Setup connection event forwarding
|
|
1648
|
+
*/
|
|
1649
|
+
setupConnectionEvents() {
|
|
1650
|
+
if (!this.connection) {
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
this.connection.on('connected', async (event) => {
|
|
1654
|
+
this.logger.info('Connection established', { sessionId: this.sessionId, agentId: this.resolvedAgentId });
|
|
1655
|
+
// Send token immediately if agent requires authentication
|
|
1656
|
+
await this.sendAuthTokenIfRequired();
|
|
1657
|
+
this.emit('connected', this.createAgentEventResponse('connected', {}, {
|
|
1658
|
+
timestamp: event.timestamp,
|
|
1659
|
+
}));
|
|
1660
|
+
// Flush queue when connected
|
|
1661
|
+
this.flushQueue();
|
|
1662
|
+
});
|
|
1663
|
+
this.connection.on('message', async (event) => {
|
|
1664
|
+
try {
|
|
1665
|
+
// Parse incoming message
|
|
1666
|
+
let sessionContext = {
|
|
1667
|
+
agentId: this.resolvedAgentId,
|
|
1668
|
+
sessionId: this.sessionId,
|
|
1669
|
+
customerName: "customer",
|
|
1670
|
+
agentName: this.agentDetails?.agentProfileDetails?.name,
|
|
1671
|
+
};
|
|
1672
|
+
const message = Message.fromJSON(event.data, sessionContext);
|
|
1673
|
+
this.logger.debug('Message received', {
|
|
1674
|
+
messageId: message.messageId,
|
|
1675
|
+
persona: message.persona,
|
|
1676
|
+
role: message.role
|
|
1677
|
+
});
|
|
1678
|
+
// Store message in transcript
|
|
1679
|
+
this.transcript.add(message, 'received', this.sessionId, this.resolvedAgentId);
|
|
1680
|
+
// Emit transcript update event
|
|
1681
|
+
this.emit('transcriptUpdate', {
|
|
1682
|
+
type: 'transcriptUpdate',
|
|
1683
|
+
timestamp: Date.now(),
|
|
1684
|
+
sessionId: this.sessionId,
|
|
1685
|
+
agentId: this.resolvedAgentId,
|
|
1686
|
+
payload: {
|
|
1687
|
+
entry: {
|
|
1688
|
+
message,
|
|
1689
|
+
direction: 'received',
|
|
1690
|
+
timestamp: message.timestamp || Date.now(),
|
|
1691
|
+
sessionId: this.sessionId,
|
|
1692
|
+
agentId: this.resolvedAgentId,
|
|
1693
|
+
},
|
|
1694
|
+
},
|
|
1695
|
+
});
|
|
1696
|
+
// Process message through handler chain
|
|
1697
|
+
const result = await this.messageProcessor.process(message);
|
|
1698
|
+
// Emit generic message event
|
|
1699
|
+
this.emit('message', this.createAgentEventResponse('message', {
|
|
1700
|
+
data: message,
|
|
1701
|
+
}));
|
|
1702
|
+
// Emit typed events based on handler result
|
|
1703
|
+
if (result) {
|
|
1704
|
+
if (result.type === 'agent_message') {
|
|
1705
|
+
this.logger.debug('Agent message processed', {
|
|
1706
|
+
messageId: message.messageId,
|
|
1707
|
+
from: result.from?.name
|
|
1708
|
+
});
|
|
1709
|
+
// Extract payload from result, excluding timestamp, sessionId, agentId
|
|
1710
|
+
const { timestamp, sessionId, agentId, ...payload } = result;
|
|
1711
|
+
this.emit('agentMessage', this.createAgentEventResponse('agentMessage', payload, {
|
|
1712
|
+
sessionId: this.sessionId ?? sessionId,
|
|
1713
|
+
agentId: this.resolvedAgentId ?? agentId,
|
|
1714
|
+
}));
|
|
1715
|
+
}
|
|
1716
|
+
else if (result.type === 'error_message') {
|
|
1717
|
+
// Create error object with message details
|
|
1718
|
+
const errorMessage = message.content || 'Error message received';
|
|
1719
|
+
const error = new Error(errorMessage);
|
|
1720
|
+
this.logger.error('Error message received', error, {
|
|
1721
|
+
messageId: message.messageId,
|
|
1722
|
+
errorCode: message.messageData?.error_code,
|
|
1723
|
+
content: message.content
|
|
1724
|
+
});
|
|
1725
|
+
// Emit errorMessage event
|
|
1726
|
+
this.emit('errorMessage', this.createAgentEventResponse('errorMessage', {
|
|
1727
|
+
message,
|
|
1728
|
+
error,
|
|
1729
|
+
}));
|
|
1730
|
+
// Optionally close connection on error (similar to reference implementation)
|
|
1731
|
+
// Consumers can handle connection closure in the errorMessage event handler if needed
|
|
1732
|
+
this.logger.debug('Error message event emitted, connection may need to be closed by consumer');
|
|
1733
|
+
}
|
|
1734
|
+
else if (result.type === 'heartbeat_processed') {
|
|
1735
|
+
this.logger.debug('Heartbeat processed', { messageId: message.messageId });
|
|
1736
|
+
// Extract payload from result, excluding timestamp, sessionId, agentId
|
|
1737
|
+
const { timestamp, sessionId, agentId, ...payload } = result;
|
|
1738
|
+
this.emit('heartbeat', this.createAgentEventResponse('heartbeat', payload, {
|
|
1739
|
+
sessionId: this.sessionId ?? sessionId,
|
|
1740
|
+
agentId: this.resolvedAgentId ?? agentId,
|
|
1741
|
+
}));
|
|
1742
|
+
}
|
|
1743
|
+
else if (result.type === 'token_refresh_required') {
|
|
1744
|
+
// Emit tokenExpiring event when transport layer requests token refresh
|
|
1745
|
+
this.logger.debug('Token refresh required by transport layer', { messageId: message.messageId });
|
|
1746
|
+
this.emit('tokenExpiring', this.createAgentEventResponse('tokenExpiring', {
|
|
1747
|
+
reason: 'transport_request',
|
|
1748
|
+
}));
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
catch (error) {
|
|
1753
|
+
// If message parsing fails, emit error but still emit raw message event
|
|
1754
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1755
|
+
this.logger.error('Failed to process message', err, { rawData: event.data });
|
|
1756
|
+
this.emit('error', this.createAgentEventResponse('error', {
|
|
1757
|
+
error: err,
|
|
1758
|
+
}));
|
|
1759
|
+
this.emit('message', this.createAgentEventResponse('message', {
|
|
1760
|
+
data: event.data,
|
|
1761
|
+
}));
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
this.connection.on('error', (event) => {
|
|
1765
|
+
this.logger.error('Connection error', event.error, {
|
|
1766
|
+
sessionId: this.sessionId,
|
|
1767
|
+
agentId: this.resolvedAgentId
|
|
1768
|
+
});
|
|
1769
|
+
this.emit('error', this.createAgentEventResponse('error', {
|
|
1770
|
+
error: event.error,
|
|
1771
|
+
}, {
|
|
1772
|
+
timestamp: event.timestamp,
|
|
1773
|
+
}));
|
|
1774
|
+
});
|
|
1775
|
+
this.connection.on('closed', (event) => {
|
|
1776
|
+
this.logger.info('Connection closed', {
|
|
1777
|
+
code: event.code,
|
|
1778
|
+
reason: event.reason,
|
|
1779
|
+
sessionId: this.sessionId,
|
|
1780
|
+
agentId: this.resolvedAgentId
|
|
1781
|
+
});
|
|
1782
|
+
this.emit('closed', this.createAgentEventResponse('closed', {
|
|
1783
|
+
code: event.code,
|
|
1784
|
+
reason: event.reason,
|
|
1785
|
+
}, {
|
|
1786
|
+
timestamp: event.timestamp,
|
|
1787
|
+
}));
|
|
1788
|
+
});
|
|
1789
|
+
this.connection.on('stateChanged', (event) => {
|
|
1790
|
+
this.logger.debug('Connection state changed', {
|
|
1791
|
+
previousState: event.previousState,
|
|
1792
|
+
newState: event.state,
|
|
1793
|
+
sessionId: this.sessionId,
|
|
1794
|
+
agentId: this.resolvedAgentId
|
|
1795
|
+
});
|
|
1796
|
+
this.emit('stateChanged', this.createAgentEventResponse('stateChanged', {
|
|
1797
|
+
state: event.state,
|
|
1798
|
+
previousState: event.previousState,
|
|
1799
|
+
}));
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Send authentication token to agent if authentication is required
|
|
1804
|
+
* This is called immediately after connection is established
|
|
1805
|
+
*/
|
|
1806
|
+
async sendAuthTokenIfRequired() {
|
|
1807
|
+
// Check if agent requires authentication (isAuthenticated flag in agent details)
|
|
1808
|
+
const isAuthenticationRequired = this.agentDetails?.isAuthenticated;
|
|
1809
|
+
if (!isAuthenticationRequired) {
|
|
1810
|
+
this.logger.debug('Authentication not required, skipping token send', { agentId: this.resolvedAgentId });
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
try {
|
|
1814
|
+
// Get the current access token
|
|
1815
|
+
const authToken = await this.authService.getToken();
|
|
1816
|
+
if (!authToken) {
|
|
1817
|
+
this.logger.warn('Authentication required but no token available', { agentId: this.resolvedAgentId });
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
// Create and send the token message
|
|
1821
|
+
const tokenMessage = createTokenMessage({ token: authToken });
|
|
1822
|
+
const payload = JSON.stringify(tokenMessage);
|
|
1823
|
+
if (this.connection?.isConnected()) {
|
|
1824
|
+
await this.connection.send(payload);
|
|
1825
|
+
this.logger.debug('Authentication token sent to agent', { agentId: this.resolvedAgentId });
|
|
1826
|
+
}
|
|
1827
|
+
else {
|
|
1828
|
+
this.logger.warn('Cannot send auth token: connection not available', { agentId: this.resolvedAgentId });
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
catch (error) {
|
|
1832
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1833
|
+
this.logger.error('Failed to send authentication token', err, { agentId: this.resolvedAgentId });
|
|
1834
|
+
// Don't throw - allow connection to continue even if token send fails
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Queue a message for later sending
|
|
1839
|
+
*/
|
|
1840
|
+
queueMessage(data, id) {
|
|
1841
|
+
try {
|
|
1842
|
+
const messageId = this.messageQueue.enqueue(data, id);
|
|
1843
|
+
this.logger.debug('Message queued', { messageId, queueSize: this.messageQueue.size() });
|
|
1844
|
+
return messageId;
|
|
1845
|
+
}
|
|
1846
|
+
catch (error) {
|
|
1847
|
+
const err = new MessageError('Failed to queue message', error instanceof Error ? error : new Error(String(error)));
|
|
1848
|
+
this.logger.error('Failed to queue message', err, { messageId: id });
|
|
1849
|
+
throw err;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* Flush queued messages when connected
|
|
1854
|
+
*/
|
|
1855
|
+
async flushQueue() {
|
|
1856
|
+
if (this.isFlushingQueue || !this.connection?.isConnected()) {
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
this.isFlushingQueue = true;
|
|
1860
|
+
let flushedCount = 0;
|
|
1861
|
+
const queueSize = this.messageQueue.size();
|
|
1862
|
+
if (queueSize > 0) {
|
|
1863
|
+
this.logger.debug('Flushing message queue', { queueSize });
|
|
1864
|
+
}
|
|
1865
|
+
try {
|
|
1866
|
+
while (!this.messageQueue.isEmpty() && this.connection?.isConnected()) {
|
|
1867
|
+
const message = this.messageQueue.peek();
|
|
1868
|
+
if (!message) {
|
|
1869
|
+
break;
|
|
1870
|
+
}
|
|
1871
|
+
try {
|
|
1872
|
+
await this.connection.send(message.data);
|
|
1873
|
+
this.messageQueue.dequeue();
|
|
1874
|
+
flushedCount++;
|
|
1875
|
+
// Small delay between messages to avoid overwhelming the connection
|
|
1876
|
+
await this.delay(5);
|
|
1877
|
+
}
|
|
1878
|
+
catch (error) {
|
|
1879
|
+
// If send fails, mark as attempted
|
|
1880
|
+
const shouldRetry = this.messageQueue.markAttempted(message.id);
|
|
1881
|
+
if (!shouldRetry) {
|
|
1882
|
+
// Max attempts reached, remove from queue
|
|
1883
|
+
this.messageQueue.remove(message.id);
|
|
1884
|
+
const err = new MessageError(`Failed to send queued message after max attempts: ${message.id}`, error instanceof Error ? error : new Error(String(error)));
|
|
1885
|
+
this.logger.error('Failed to send queued message after max attempts', err, { messageId: message.id });
|
|
1886
|
+
this.emit('error', this.createAgentEventResponse('error', {
|
|
1887
|
+
error: err,
|
|
1888
|
+
}));
|
|
1889
|
+
}
|
|
1890
|
+
else {
|
|
1891
|
+
// Wait a bit before retrying
|
|
1892
|
+
this.logger.debug('Retrying queued message', { messageId: message.id });
|
|
1893
|
+
await this.delay(100);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
if (flushedCount > 0) {
|
|
1898
|
+
this.logger.info('Message queue flushed', { count: flushedCount });
|
|
1899
|
+
this.emit('queueFlushed', this.createAgentEventResponse('queueFlushed', {
|
|
1900
|
+
count: flushedCount,
|
|
1901
|
+
}));
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
finally {
|
|
1905
|
+
this.isFlushingQueue = false;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Create an AgentEvent object with automatic timestamp, sessionId, and agentId
|
|
1910
|
+
* @param type - The event type
|
|
1911
|
+
* @param payload - The event-specific payload data
|
|
1912
|
+
* @param options - Optional overrides for timestamp, sessionId, or agentId
|
|
1913
|
+
* @returns A properly formatted AgentEvent object
|
|
1914
|
+
*/
|
|
1915
|
+
createAgentEventResponse(type, payload, options) {
|
|
1916
|
+
return {
|
|
1917
|
+
type,
|
|
1918
|
+
timestamp: options?.timestamp ?? Date.now(),
|
|
1919
|
+
sessionId: options?.sessionId ?? this.sessionId,
|
|
1920
|
+
agentId: options?.agentId ?? this.resolvedAgentId,
|
|
1921
|
+
payload,
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Generate a unique message ID
|
|
1926
|
+
*/
|
|
1927
|
+
generateMessageId() {
|
|
1928
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Delay utility
|
|
1932
|
+
*/
|
|
1933
|
+
delay(ms) {
|
|
1934
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Get the message processor instance
|
|
1938
|
+
* Allows adding custom handlers
|
|
1939
|
+
*/
|
|
1940
|
+
getMessageProcessor() {
|
|
1941
|
+
return this.messageProcessor;
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Get the current access token from the authentication strategy
|
|
1945
|
+
* Returns the access token that the agent is currently using for authentication
|
|
1946
|
+
* @returns Promise resolving to the access token string, or null if no token is available
|
|
1947
|
+
*
|
|
1948
|
+
* @example
|
|
1949
|
+
* ```typescript
|
|
1950
|
+
* const token = await agent.getAccessToken();
|
|
1951
|
+
* if (token) {
|
|
1952
|
+
* // Use the token for external API calls
|
|
1953
|
+
* fetch('https://api.example.com/data', {
|
|
1954
|
+
* headers: { Authorization: `Bearer ${token}` }
|
|
1955
|
+
* });
|
|
1956
|
+
* }
|
|
1957
|
+
* ```
|
|
1958
|
+
*/
|
|
1959
|
+
async getAccessToken() {
|
|
1960
|
+
this.logger.debug('Getting access token', { agentId: this.resolvedAgentId });
|
|
1961
|
+
try {
|
|
1962
|
+
const token = await this.authService.getToken();
|
|
1963
|
+
if (token) {
|
|
1964
|
+
this.logger.debug('Access token retrieved', { agentId: this.resolvedAgentId, tokenLength: token.length });
|
|
1965
|
+
}
|
|
1966
|
+
else {
|
|
1967
|
+
this.logger.debug('No access token available', { agentId: this.resolvedAgentId });
|
|
1968
|
+
}
|
|
1969
|
+
return token;
|
|
1970
|
+
}
|
|
1971
|
+
catch (error) {
|
|
1972
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1973
|
+
this.logger.error('Failed to get access token', err, { agentId: this.resolvedAgentId });
|
|
1974
|
+
throw error;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Update the access token at runtime
|
|
1979
|
+
* Use this method when you receive a tokenExpiring event to provide a new token
|
|
1980
|
+
* @param token - The new access token
|
|
1981
|
+
* @throws Error if the authentication strategy doesn't support token updates
|
|
1982
|
+
*
|
|
1983
|
+
* @example
|
|
1984
|
+
* ```typescript
|
|
1985
|
+
* agent.on('tokenExpiring', async (event) => {
|
|
1986
|
+
* const newToken = await fetchNewTokenFromServer();
|
|
1987
|
+
* await agent.updateAccessToken(newToken);
|
|
1988
|
+
* });
|
|
1989
|
+
* ```
|
|
1990
|
+
*/
|
|
1991
|
+
async updateAccessToken(token) {
|
|
1992
|
+
this.logger.debug('Updating access token', { agentId: this.resolvedAgentId });
|
|
1993
|
+
await this.authService.updateToken(token);
|
|
1994
|
+
this.logger.info('Access token updated successfully', { agentId: this.resolvedAgentId });
|
|
1995
|
+
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Set or update the session ID at runtime
|
|
1998
|
+
* Updates the sessionId property, which will be used for future connections and logging.
|
|
1999
|
+
*
|
|
2000
|
+
* **Note:** If the agent is currently connected, changing the sessionId will not automatically
|
|
2001
|
+
* switch the connection to the new session. You should either:
|
|
2002
|
+
* - Disconnect and reconnect with the new sessionId
|
|
2003
|
+
* - Use `restartConnection({ sessionId: newSessionId })` to restart with the new session
|
|
2004
|
+
*
|
|
2005
|
+
* @param sessionId - The new session ID to set
|
|
2006
|
+
*
|
|
2007
|
+
* @example
|
|
2008
|
+
* ```typescript
|
|
2009
|
+
* // Update sessionId after initialization
|
|
2010
|
+
* agent.setSessionId('new-session-id');
|
|
2011
|
+
*
|
|
2012
|
+
* // If connected, restart with the new sessionId
|
|
2013
|
+
* if (agent.isConnected()) {
|
|
2014
|
+
* await agent.restartConnection({ sessionId: 'new-session-id' });
|
|
2015
|
+
* }
|
|
2016
|
+
* ```
|
|
2017
|
+
*/
|
|
2018
|
+
setSessionId(sessionId) {
|
|
2019
|
+
const oldSessionId = this.sessionId;
|
|
2020
|
+
this.sessionId = sessionId;
|
|
2021
|
+
this.logger.info('SessionId updated', {
|
|
2022
|
+
agentId: this.resolvedAgentId,
|
|
2023
|
+
oldSessionId,
|
|
2024
|
+
newSessionId: sessionId,
|
|
2025
|
+
isConnected: this.isConnected()
|
|
2026
|
+
});
|
|
2027
|
+
// Warn if connected - changing sessionId while connected won't affect the current connection
|
|
2028
|
+
if (this.isConnected()) {
|
|
2029
|
+
this.logger.warn('SessionId updated while connected. Current connection still uses old sessionId. Consider using restartConnection() to switch to the new session.', {
|
|
2030
|
+
agentId: this.resolvedAgentId,
|
|
2031
|
+
currentSessionId: oldSessionId,
|
|
2032
|
+
newSessionId: sessionId
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
//# sourceMappingURL=AiAgent.js.map
|