@webex/contact-center 3.12.0-next.9 → 3.12.0-task-refactor.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/AGENTS.md +438 -0
  2. package/ai-docs/README.md +131 -0
  3. package/ai-docs/RULES.md +455 -0
  4. package/ai-docs/patterns/event-driven-patterns.md +485 -0
  5. package/ai-docs/patterns/testing-patterns.md +480 -0
  6. package/ai-docs/patterns/typescript-patterns.md +365 -0
  7. package/ai-docs/templates/README.md +102 -0
  8. package/ai-docs/templates/documentation/create-agents-md.md +240 -0
  9. package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
  10. package/ai-docs/templates/existing-service/bug-fix.md +254 -0
  11. package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
  12. package/ai-docs/templates/new-method/00-master.md +80 -0
  13. package/ai-docs/templates/new-method/01-requirements.md +232 -0
  14. package/ai-docs/templates/new-method/02-implementation.md +295 -0
  15. package/ai-docs/templates/new-method/03-tests.md +201 -0
  16. package/ai-docs/templates/new-method/04-validation.md +141 -0
  17. package/ai-docs/templates/new-service/00-master.md +109 -0
  18. package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
  19. package/ai-docs/templates/new-service/02-code-generation.md +346 -0
  20. package/ai-docs/templates/new-service/03-integration.md +178 -0
  21. package/ai-docs/templates/new-service/04-test-generation.md +205 -0
  22. package/ai-docs/templates/new-service/05-validation.md +145 -0
  23. package/dist/cc.js +65 -123
  24. package/dist/cc.js.map +1 -1
  25. package/dist/constants.js +13 -2
  26. package/dist/constants.js.map +1 -1
  27. package/dist/index.js +13 -5
  28. package/dist/index.js.map +1 -1
  29. package/dist/metrics/behavioral-events.js +26 -13
  30. package/dist/metrics/behavioral-events.js.map +1 -1
  31. package/dist/metrics/constants.js +7 -6
  32. package/dist/metrics/constants.js.map +1 -1
  33. package/dist/services/ApiAiAssistant.js +0 -3
  34. package/dist/services/ApiAiAssistant.js.map +1 -1
  35. package/dist/services/config/Util.js +2 -3
  36. package/dist/services/config/Util.js.map +1 -1
  37. package/dist/services/config/types.js +16 -14
  38. package/dist/services/config/types.js.map +1 -1
  39. package/dist/services/constants.js +0 -1
  40. package/dist/services/constants.js.map +1 -1
  41. package/dist/services/core/Err.js.map +1 -1
  42. package/dist/services/core/Utils.js +79 -55
  43. package/dist/services/core/Utils.js.map +1 -1
  44. package/dist/services/core/aqm-reqs.js +17 -92
  45. package/dist/services/core/aqm-reqs.js.map +1 -1
  46. package/dist/services/core/websocket/WebSocketManager.js +5 -25
  47. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  48. package/dist/services/core/websocket/types.js.map +1 -1
  49. package/dist/services/index.js +1 -2
  50. package/dist/services/index.js.map +1 -1
  51. package/dist/services/task/Task.js +644 -0
  52. package/dist/services/task/Task.js.map +1 -0
  53. package/dist/services/task/TaskFactory.js +45 -0
  54. package/dist/services/task/TaskFactory.js.map +1 -0
  55. package/dist/services/task/TaskManager.js +556 -532
  56. package/dist/services/task/TaskManager.js.map +1 -1
  57. package/dist/services/task/TaskUtils.js +132 -28
  58. package/dist/services/task/TaskUtils.js.map +1 -1
  59. package/dist/services/task/constants.js +7 -6
  60. package/dist/services/task/constants.js.map +1 -1
  61. package/dist/services/task/dialer.js +0 -51
  62. package/dist/services/task/dialer.js.map +1 -1
  63. package/dist/services/task/digital/Digital.js +77 -0
  64. package/dist/services/task/digital/Digital.js.map +1 -0
  65. package/dist/services/task/state-machine/TaskStateMachine.js +634 -0
  66. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
  67. package/dist/services/task/state-machine/actions.js +366 -0
  68. package/dist/services/task/state-machine/actions.js.map +1 -0
  69. package/dist/services/task/state-machine/constants.js +139 -0
  70. package/dist/services/task/state-machine/constants.js.map +1 -0
  71. package/dist/services/task/state-machine/guards.js +256 -0
  72. package/dist/services/task/state-machine/guards.js.map +1 -0
  73. package/dist/services/task/state-machine/index.js +53 -0
  74. package/dist/services/task/state-machine/index.js.map +1 -0
  75. package/dist/services/task/state-machine/types.js +54 -0
  76. package/dist/services/task/state-machine/types.js.map +1 -0
  77. package/dist/services/task/state-machine/uiControlsComputer.js +369 -0
  78. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
  79. package/dist/services/task/taskDataNormalizer.js +99 -0
  80. package/dist/services/task/taskDataNormalizer.js.map +1 -0
  81. package/dist/services/task/types.js +157 -18
  82. package/dist/services/task/types.js.map +1 -1
  83. package/dist/services/task/voice/Voice.js +1031 -0
  84. package/dist/services/task/voice/Voice.js.map +1 -0
  85. package/dist/services/task/voice/WebRTC.js +149 -0
  86. package/dist/services/task/voice/WebRTC.js.map +1 -0
  87. package/dist/types/cc.d.ts +4 -33
  88. package/dist/types/constants.d.ts +13 -2
  89. package/dist/types/index.d.ts +11 -5
  90. package/dist/types/metrics/constants.d.ts +5 -3
  91. package/dist/types/services/ApiAiAssistant.d.ts +1 -1
  92. package/dist/types/services/config/types.d.ts +97 -25
  93. package/dist/types/services/core/Err.d.ts +0 -2
  94. package/dist/types/services/core/Utils.d.ts +25 -23
  95. package/dist/types/services/core/aqm-reqs.d.ts +0 -49
  96. package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -1
  97. package/dist/types/services/core/websocket/connection-service.d.ts +0 -1
  98. package/dist/types/services/core/websocket/types.d.ts +1 -1
  99. package/dist/types/services/index.d.ts +1 -1
  100. package/dist/types/services/task/Task.d.ts +146 -0
  101. package/dist/types/services/task/TaskFactory.d.ts +12 -0
  102. package/dist/types/services/task/TaskUtils.d.ts +39 -8
  103. package/dist/types/services/task/constants.d.ts +5 -4
  104. package/dist/types/services/task/dialer.d.ts +0 -15
  105. package/dist/types/services/task/digital/Digital.d.ts +22 -0
  106. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +906 -0
  107. package/dist/types/services/task/state-machine/actions.d.ts +8 -0
  108. package/dist/types/services/task/state-machine/constants.d.ts +91 -0
  109. package/dist/types/services/task/state-machine/guards.d.ts +78 -0
  110. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  111. package/dist/types/services/task/state-machine/types.d.ts +256 -0
  112. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  113. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  114. package/dist/types/services/task/types.d.ts +539 -88
  115. package/dist/types/services/task/voice/Voice.d.ts +183 -0
  116. package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
  117. package/dist/types/types.d.ts +68 -0
  118. package/dist/types/webex.d.ts +1 -0
  119. package/dist/types.js +70 -0
  120. package/dist/types.js.map +1 -1
  121. package/dist/webex.js +14 -2
  122. package/dist/webex.js.map +1 -1
  123. package/package.json +14 -11
  124. package/src/cc.ts +91 -177
  125. package/src/constants.ts +13 -2
  126. package/src/index.ts +14 -5
  127. package/src/metrics/ai-docs/AGENTS.md +348 -0
  128. package/src/metrics/ai-docs/ARCHITECTURE.md +336 -0
  129. package/src/metrics/behavioral-events.ts +28 -14
  130. package/src/metrics/constants.ts +7 -8
  131. package/src/services/ApiAiAssistant.ts +2 -4
  132. package/src/services/agent/ai-docs/AGENTS.md +238 -0
  133. package/src/services/agent/ai-docs/ARCHITECTURE.md +302 -0
  134. package/src/services/ai-docs/AGENTS.md +384 -0
  135. package/src/services/config/Util.ts +2 -3
  136. package/src/services/config/ai-docs/AGENTS.md +253 -0
  137. package/src/services/config/ai-docs/ARCHITECTURE.md +424 -0
  138. package/src/services/config/types.ts +108 -20
  139. package/src/services/constants.ts +0 -1
  140. package/src/services/core/Err.ts +0 -1
  141. package/src/services/core/Utils.ts +90 -67
  142. package/src/services/core/ai-docs/AGENTS.md +379 -0
  143. package/src/services/core/ai-docs/ARCHITECTURE.md +696 -0
  144. package/src/services/core/aqm-reqs.ts +22 -100
  145. package/src/services/core/websocket/WebSocketManager.ts +4 -23
  146. package/src/services/core/websocket/types.ts +1 -1
  147. package/src/services/index.ts +1 -2
  148. package/src/services/task/Task.ts +785 -0
  149. package/src/services/task/TaskFactory.ts +55 -0
  150. package/src/services/task/TaskManager.ts +567 -633
  151. package/src/services/task/TaskUtils.ts +175 -31
  152. package/src/services/task/ai-docs/AGENTS.md +448 -0
  153. package/src/services/task/ai-docs/ARCHITECTURE.md +573 -0
  154. package/src/services/task/constants.ts +5 -4
  155. package/src/services/task/dialer.ts +1 -56
  156. package/src/services/task/digital/Digital.ts +95 -0
  157. package/src/services/task/state-machine/TaskStateMachine.ts +793 -0
  158. package/src/services/task/state-machine/actions.ts +409 -0
  159. package/src/services/task/state-machine/ai-docs/AGENTS.md +495 -0
  160. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1135 -0
  161. package/src/services/task/state-machine/constants.ts +150 -0
  162. package/src/services/task/state-machine/guards.ts +295 -0
  163. package/src/services/task/state-machine/index.ts +28 -0
  164. package/src/services/task/state-machine/types.ts +228 -0
  165. package/src/services/task/state-machine/uiControlsComputer.ts +529 -0
  166. package/src/services/task/taskDataNormalizer.ts +137 -0
  167. package/src/services/task/types.ts +641 -95
  168. package/src/services/task/voice/Voice.ts +1255 -0
  169. package/src/services/task/voice/WebRTC.ts +187 -0
  170. package/src/types.ts +88 -5
  171. package/src/utils/AGENTS.md +276 -0
  172. package/src/webex.js +2 -0
  173. package/test/unit/spec/cc.ts +59 -142
  174. package/test/unit/spec/logger-proxy.ts +70 -0
  175. package/test/unit/spec/services/ApiAiAssistant.ts +17 -0
  176. package/test/unit/spec/services/config/index.ts +26 -55
  177. package/test/unit/spec/services/core/Utils.ts +103 -52
  178. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +48 -112
  179. package/test/unit/spec/services/core/websocket/connection-service.ts +5 -4
  180. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  181. package/test/unit/spec/services/task/Task.ts +416 -0
  182. package/test/unit/spec/services/task/TaskFactory.ts +62 -0
  183. package/test/unit/spec/services/task/TaskManager.ts +781 -1735
  184. package/test/unit/spec/services/task/TaskUtils.ts +125 -0
  185. package/test/unit/spec/services/task/dialer.ts +112 -198
  186. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  187. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +473 -0
  188. package/test/unit/spec/services/task/state-machine/guards.ts +288 -0
  189. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  190. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +147 -0
  191. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  192. package/test/unit/spec/services/task/voice/Voice.ts +587 -0
  193. package/test/unit/spec/services/task/voice/WebRTC.ts +242 -0
  194. package/umd/contact-center.min.js +2 -2
  195. package/umd/contact-center.min.js.map +1 -1
  196. package/dist/services/task/index.js +0 -1525
  197. package/dist/services/task/index.js.map +0 -1
  198. package/dist/types/services/task/index.d.ts +0 -650
  199. package/src/services/task/index.ts +0 -1801
  200. package/test/unit/spec/services/task/index.ts +0 -2184
@@ -3,25 +3,32 @@ import {ICall, LINE_EVENTS} from '@webex/calling';
3
3
  import {WebSocketManager} from '../core/websocket/WebSocketManager';
4
4
  import routingContact from './contact';
5
5
  import WebCallingService from '../WebCallingService';
6
- import {ITask, MEDIA_CHANNEL, TASK_EVENTS, TaskData, TaskId} from './types';
6
+ import {
7
+ MEDIA_CHANNEL,
8
+ TASK_EVENTS,
9
+ TaskData,
10
+ TaskId,
11
+ ITask,
12
+ WebSocketPayload,
13
+ WebSocketMessage,
14
+ TaskEventActions,
15
+ EventContext,
16
+ } from './types';
7
17
  import {TASK_MANAGER_FILE} from '../../constants';
8
18
  import {METHODS, TRANSCRIPT_EVENT_MAP} from './constants';
9
- import {CC_EVENTS, CC_TASK_EVENTS, WrapupData} from '../config/types';
10
- import {AIAssistantEventName, AIAssistantEventType, LoginOption} from '../../types';
19
+ import {CC_EVENTS, WrapupData} from '../config/types';
20
+ import {ConfigFlags, LoginOption, AIAssistantEventType, AIAssistantEventName} from '../../types';
11
21
  import LoggerProxy from '../../logger-proxy';
12
- import Task from '.';
13
- import MetricsManager from '../../metrics/MetricsManager';
14
- import {METRIC_EVENT_NAMES} from '../../metrics/constants';
15
- import {
16
- checkParticipantNotInInteraction,
17
- getIsConferenceInProgress,
18
- isCampaignPreviewReservation,
19
- isParticipantInMainInteraction,
20
- isPrimary,
21
- isSecondaryEpDnAgent,
22
- shouldAutoAnswerTask,
23
- } from './TaskUtils';
24
- import ApiAIAssistant from '../ApiAiAssistant';
22
+ import {getIsConferenceInProgress, isSecondaryEpDnAgent, shouldAutoAnswerTask} from './TaskUtils';
23
+ import TaskFactory from './TaskFactory';
24
+ import WebRTC from './voice/WebRTC';
25
+ import {TaskEvent, type TaskEventPayload} from './state-machine';
26
+ import {normalizeTaskData} from './taskDataNormalizer';
27
+ import {ApiAIAssistant} from '../ApiAiAssistant';
28
+
29
+ const CC_EVENT_SET = new Set<CC_EVENTS>(Object.values(CC_EVENTS) as CC_EVENTS[]);
30
+
31
+ const isCcEvent = (value: string): value is CC_EVENTS => CC_EVENT_SET.has(value as CC_EVENTS);
25
32
 
26
33
  /** @internal */
27
34
  export default class TaskManager extends EventEmitter {
@@ -35,8 +42,10 @@ export default class TaskManager extends EventEmitter {
35
42
  private taskCollection: Record<TaskId, ITask>;
36
43
  private webCallingService: WebCallingService;
37
44
  private webSocketManager: WebSocketManager;
38
- private metricsManager: MetricsManager;
39
- private static taskManager;
45
+ private rtdWebSocketManager: WebSocketManager;
46
+ // eslint-disable-next-line no-use-before-define
47
+ private static taskManager: TaskManager;
48
+ private configFlags?: ConfigFlags;
40
49
  private wrapupData: WrapupData;
41
50
  private agentId: string;
42
51
  private webRtcEnabled: boolean;
@@ -50,47 +59,28 @@ export default class TaskManager extends EventEmitter {
50
59
  apiAIAssistant: ApiAIAssistant,
51
60
  contact: ReturnType<typeof routingContact>,
52
61
  webCallingService: WebCallingService,
53
- webSocketManager: WebSocketManager
62
+ webSocketManager: WebSocketManager,
63
+ rtdWebSocketManager: WebSocketManager
54
64
  ) {
55
65
  super();
56
66
  this.apiAIAssistant = apiAIAssistant;
57
67
  this.contact = contact;
58
- this.taskCollection = {};
59
68
  this.webCallingService = webCallingService;
60
69
  this.webSocketManager = webSocketManager;
61
- this.metricsManager = MetricsManager.getInstance();
70
+ this.rtdWebSocketManager = rtdWebSocketManager;
71
+ this.taskCollection = {};
72
+ this.webRtcEnabled = false;
73
+
62
74
  this.registerTaskListeners();
63
75
  this.registerIncomingCallEvent();
64
76
  }
65
77
 
66
- public setWrapupData(wrapupData: WrapupData) {
67
- this.wrapupData = wrapupData;
68
- }
69
-
70
- public setAgentId(agentId: string) {
71
- this.agentId = agentId;
72
- }
73
-
74
- /**
75
- * Gets the current agent ID
76
- * @returns {string} The agent ID set for this task manager instance
77
- * @public
78
- */
79
- public getAgentId(): string {
80
- return this.agentId;
81
- }
82
-
83
- public setWebRtcEnabled(webRtcEnabled: boolean) {
84
- this.webRtcEnabled = webRtcEnabled;
85
- }
86
-
87
78
  public handleRealtimeWebsocketEvent(event: string) {
88
79
  try {
89
80
  const payload = JSON.parse(event);
90
81
 
91
- const eventType = payload?.type || payload?.data?.notifType;
92
82
  const interactionId = payload?.data?.data?.conversationId;
93
- if (!eventType || !interactionId) return;
83
+ if (!interactionId) return;
94
84
 
95
85
  const task = this.taskCollection[interactionId];
96
86
  if (!task) {
@@ -103,7 +93,7 @@ export default class TaskManager extends EventEmitter {
103
93
  return;
104
94
  }
105
95
 
106
- task.emit(eventType, payload.data);
96
+ task.emit(payload.type, payload.data);
107
97
  } catch (error) {
108
98
  LoggerProxy.error('Failed to parse RTD WebSocket message', {
109
99
  module: TASK_MANAGER_FILE,
@@ -113,10 +103,43 @@ export default class TaskManager extends EventEmitter {
113
103
  }
114
104
  }
115
105
 
106
+ /**
107
+ * Set config flags for task creation
108
+ */
109
+ public setConfigFlags(configFlags: ConfigFlags) {
110
+ this.configFlags = configFlags;
111
+ }
112
+
113
+ /**
114
+ * Set wrapup configuration data
115
+ */
116
+ public setWrapupData(wrapupData: WrapupData) {
117
+ this.wrapupData = wrapupData;
118
+ }
119
+
120
+ /**
121
+ * Set agent ID for task operations
122
+ */
123
+ public setAgentId(agentId: string) {
124
+ this.agentId = agentId;
125
+ }
126
+
127
+ /**
128
+ * Gets the current agent ID
129
+ * @returns {string} The agent ID set for this task manager instance
130
+ * @public
131
+ */
132
+ public getAgentId(): string {
133
+ return this.agentId;
134
+ }
135
+
136
+ public setWebRtcEnabled(webRtcEnabled: boolean) {
137
+ this.webRtcEnabled = webRtcEnabled;
138
+ }
139
+
116
140
  private handleIncomingWebCall = (call: ICall) => {
117
141
  const currentTask = Object.values(this.taskCollection).find(
118
- (task) =>
119
- task.data.interaction.mediaType === 'telephony' && !isCampaignPreviewReservation(task)
142
+ (t) => t.data.interaction.mediaType === MEDIA_CHANNEL.TELEPHONY
120
143
  );
121
144
 
122
145
  if (currentTask) {
@@ -126,7 +149,15 @@ export default class TaskManager extends EventEmitter {
126
149
  method: METHODS.HANDLE_INCOMING_WEB_CALL,
127
150
  interactionId: currentTask.data.interactionId,
128
151
  });
129
- this.emit(TASK_EVENTS.TASK_INCOMING, currentTask);
152
+
153
+ // Send TASK_INCOMING to state machine - it will emit on the task object
154
+ const eventPayload = TaskManager.mapEventToTaskStateMachineEvent(
155
+ CC_EVENTS.AGENT_CONTACT_RESERVED,
156
+ currentTask.data
157
+ );
158
+ if (eventPayload && currentTask) {
159
+ currentTask.sendStateMachineEvent(eventPayload);
160
+ }
130
161
  }
131
162
  this.call = call;
132
163
  };
@@ -139,551 +170,470 @@ export default class TaskManager extends EventEmitter {
139
170
  this.webCallingService.off(LINE_EVENTS.INCOMING_CALL, this.handleIncomingWebCall);
140
171
  }
141
172
 
173
+ /**
174
+ * Map WebSocket CC_EVENTS to state machine TaskEvent
175
+ * @param ccEvent - The CC_EVENT type from WebSocket
176
+ * @param payload - The event payload
177
+ * @param agentId - Optional agent ID for state detection (needed for HYDRATE)
178
+ * @returns TaskEventPayload for state machine or null if no mapping
179
+ */
180
+ private static mapEventToTaskStateMachineEvent(
181
+ ccEvent: CC_EVENTS,
182
+ payload: WebSocketPayload,
183
+ agentId?: string
184
+ ): TaskEventPayload | null {
185
+ const mediaResourceId =
186
+ payload.mediaResourceId ||
187
+ payload.interaction?.media?.[payload.interactionId]?.mediaResourceId;
188
+
189
+ switch (ccEvent) {
190
+ // CC -> TaskEvent mappings (see TaskStateMachine comment for quick reference)
191
+ case CC_EVENTS.AGENT_CONTACT_RESERVED: // AgentContactReserved -> TASK_INCOMING
192
+ return {type: TaskEvent.TASK_INCOMING, taskData: payload};
193
+
194
+ case CC_EVENTS.AGENT_OFFER_CONTACT: // AgentOfferContact -> TASK_OFFERED
195
+ return {type: TaskEvent.TASK_OFFERED, taskData: payload};
196
+
197
+ case CC_EVENTS.AGENT_CONTACT: // AgentContact -> HYDRATE
198
+ // Include agentId for state detection (e.g., checking isWrapUp in participant data)
199
+ return {type: TaskEvent.HYDRATE, taskData: payload, agentId};
200
+
201
+ case CC_EVENTS.CONTACT_UPDATED:
202
+ return {type: TaskEvent.CONTACT_UPDATED, taskData: payload};
203
+ case CC_EVENTS.CONTACT_OWNER_CHANGED:
204
+ return {type: TaskEvent.CONTACT_OWNER_CHANGED, taskData: payload};
205
+
206
+ case CC_EVENTS.AGENT_OFFER_CONSULT: // AgentOfferConsult -> OFFER_CONSULT
207
+ return {
208
+ type: TaskEvent.OFFER_CONSULT,
209
+ taskData: {...payload, isConsulted: true},
210
+ };
211
+
212
+ case CC_EVENTS.AGENT_CONTACT_ASSIGNED: // AgentContactAssigned -> ASSIGN
213
+ return {type: TaskEvent.ASSIGN, taskData: payload};
214
+
215
+ case CC_EVENTS.AGENT_CONTACT_HELD:
216
+ return {
217
+ type: TaskEvent.HOLD_SUCCESS,
218
+ mediaResourceId: mediaResourceId || '',
219
+ taskData: payload,
220
+ };
221
+
222
+ case CC_EVENTS.AGENT_CONTACT_UNHELD:
223
+ return {
224
+ type: TaskEvent.UNHOLD_SUCCESS,
225
+ mediaResourceId: mediaResourceId || '',
226
+ taskData: payload,
227
+ };
228
+
229
+ case CC_EVENTS.AGENT_CONSULT_CREATED:
230
+ return {
231
+ type: TaskEvent.CONSULT_CREATED,
232
+ taskData: {...payload, isConsulted: false},
233
+ };
234
+
235
+ case CC_EVENTS.AGENT_CONSULTING: // AgentConsulting -> CONSULTING_ACTIVE
236
+ // use context to figure out if it's the initiator or receiver using consultInitiator from context
237
+ return {
238
+ type: TaskEvent.CONSULTING_ACTIVE,
239
+ consultDestinationAgentJoined: true,
240
+ taskData: payload,
241
+ };
242
+
243
+ case CC_EVENTS.AGENT_CONSULT_ENDED: // AgentConsultEnded -> CONSULT_END
244
+ return {type: TaskEvent.CONSULT_END, taskData: payload};
245
+
246
+ case CC_EVENTS.AGENT_CONSULT_FAILED:
247
+ case CC_EVENTS.AGENT_CTQ_FAILED:
248
+ return {type: TaskEvent.CONSULT_FAILED, reason: payload.reason, taskData: payload};
249
+
250
+ case CC_EVENTS.AGENT_CTQ_CANCELLED:
251
+ return {type: TaskEvent.CTQ_CANCEL, taskData: payload};
252
+
253
+ case CC_EVENTS.AGENT_CTQ_CANCEL_FAILED:
254
+ return {type: TaskEvent.CTQ_CANCEL_FAILED, taskData: payload};
255
+
256
+ case CC_EVENTS.AGENT_BLIND_TRANSFERRED: // AgentBlindTransferred -> TRANSFER_SUCCESS
257
+ case CC_EVENTS.AGENT_CONSULT_TRANSFERRED: // AgentConsultTransferred -> TRANSFER_SUCCESS
258
+ case CC_EVENTS.AGENT_VTEAM_TRANSFERRED: // AgentVTeamTransferred -> TRANSFER_SUCCESS
259
+ return {
260
+ type: TaskEvent.TRANSFER_SUCCESS,
261
+ taskData: payload,
262
+ };
263
+
264
+ case CC_EVENTS.AGENT_WRAPUP:
265
+ return {type: TaskEvent.TASK_WRAPUP, taskData: {...payload, wrapUpRequired: true}};
266
+ case CC_EVENTS.AGENT_CONTACT_UNASSIGNED:
267
+ return null; // Add WRAPUP if needed
268
+
269
+ case CC_EVENTS.AGENT_BLIND_TRANSFER_FAILED:
270
+ case CC_EVENTS.AGENT_VTEAM_TRANSFER_FAILED:
271
+ case CC_EVENTS.AGENT_CONSULT_TRANSFER_FAILED:
272
+ case CC_EVENTS.AGENT_CONFERENCE_TRANSFER_FAILED:
273
+ return {type: TaskEvent.TRANSFER_FAILED, taskData: payload};
274
+
275
+ case CC_EVENTS.CONTACT_ENDED:
276
+ return {
277
+ type: TaskEvent.CONTACT_ENDED,
278
+ taskData: {
279
+ ...payload,
280
+ wrapUpRequired: payload.interaction?.state !== 'new',
281
+ },
282
+ };
283
+
284
+ case CC_EVENTS.AGENT_INVITE_FAILED:
285
+ return {type: TaskEvent.INVITE_FAILED, reason: payload.reason};
286
+
287
+ case CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED:
288
+ return {type: TaskEvent.ASSIGN_FAILED, reason: payload.reason};
289
+
290
+ case CC_EVENTS.AGENT_CONTACT_OFFER_RONA:
291
+ return {type: TaskEvent.RONA, taskData: payload, reason: payload.reason};
292
+
293
+ case CC_EVENTS.AGENT_OUTBOUND_FAILED:
294
+ return {type: TaskEvent.OUTBOUND_FAILED, reason: payload.reason};
295
+
296
+ case CC_EVENTS.CONTACT_RECORDING_STARTED:
297
+ return {type: TaskEvent.RECORDING_STARTED, taskData: payload};
298
+
299
+ case CC_EVENTS.CONTACT_RECORDING_PAUSED:
300
+ return {type: TaskEvent.PAUSE_RECORDING, taskData: payload};
301
+
302
+ case CC_EVENTS.CONTACT_RECORDING_RESUMED:
303
+ return {type: TaskEvent.RESUME_RECORDING, taskData: payload};
304
+
305
+ case CC_EVENTS.AGENT_WRAPPEDUP:
306
+ return {type: TaskEvent.WRAPUP_COMPLETE, taskData: payload};
307
+
308
+ // Conference events - these trigger state machine transition to CONFERENCING
309
+ case CC_EVENTS.AGENT_CONSULT_CONFERENCED:
310
+ case CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE:
311
+ return {type: TaskEvent.CONFERENCE_START, taskData: payload};
312
+
313
+ case CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED:
314
+ return {type: TaskEvent.CONFERENCE_FAILED, reason: payload.reason, taskData: payload};
315
+
316
+ case CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED:
317
+ return {type: TaskEvent.CONFERENCE_END, taskData: payload};
318
+
319
+ case CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE:
320
+ return {
321
+ type: TaskEvent.PARTICIPANT_LEAVE,
322
+ taskData: payload,
323
+ participantId: payload?.participantId,
324
+ };
325
+
326
+ case CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED:
327
+ return {type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: payload};
328
+
329
+ default:
330
+ // Not all events need state machine mapping
331
+ return null;
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Register WebSocket message listeners for task events
337
+ *
338
+ * Main entry point that orchestrates event processing through a clear pipeline:
339
+ * 1. Parse and validate incoming WebSocket messages
340
+ * 2. Prepare event context with task and state machine mappings
341
+ * 3. Handle task lifecycle (creation, updates, collection management)
342
+ * 4. Send events to state machine (task-level transitions/emissions)
343
+ * 5. Cleanup is triggered via task events emitted by the state machine
344
+ *
345
+ * This architecture separates concerns:
346
+ * - TaskManager: Manages task collection lifecycle and operational concerns
347
+ * - State Machine: Manages individual task state and event emissions
348
+ */
142
349
  private registerTaskListeners() {
143
350
  this.webSocketManager.on('message', (event) => {
144
- const payload = JSON.parse(event);
145
- // Re-emit the task events to the task object
146
- let task: ITask;
147
- if (payload.data?.type || payload.type) {
148
- if (Object.values(CC_TASK_EVENTS).includes(payload.data.type || payload.type)) {
149
- task =
150
- this.taskCollection[payload.data?.interactionId] ||
151
- this.taskCollection[payload.data?.data?.conversationId];
152
- }
153
- LoggerProxy.info(`Handling task event ${payload.data?.type}`, {
154
- module: TASK_MANAGER_FILE,
155
- method: METHODS.REGISTER_TASK_LISTENERS,
156
- interactionId: payload.data?.interactionId,
157
- });
158
- switch (payload.data.type) {
159
- case CC_EVENTS.AGENT_CONTACT:
160
- // Case1 : Task is already present in taskCollection
161
- if (this.taskCollection[payload.data.interactionId]) {
162
- LoggerProxy.log(`Got AGENT_CONTACT: Task already exists in collection`, {
163
- module: TASK_MANAGER_FILE,
164
- method: METHODS.REGISTER_TASK_LISTENERS,
165
- interactionId: payload.data.interactionId,
166
- });
167
- break;
168
- } else if (!this.taskCollection[payload.data.interactionId]) {
169
- // Case2 : Task is not present in taskCollection
170
- LoggerProxy.log(`Got AGENT_CONTACT : Creating new task in taskManager`, {
171
- module: TASK_MANAGER_FILE,
172
- method: METHODS.REGISTER_TASK_LISTENERS,
173
- interactionId: payload.data.interactionId,
174
- });
175
-
176
- // Check if auto-answer should happen for this task
177
- const shouldAutoAnswer = shouldAutoAnswerTask(
178
- payload.data,
179
- this.agentId,
180
- this.webCallingService.loginOption,
181
- this.webRtcEnabled
182
- );
183
-
184
- task = new Task(
185
- this.contact,
186
- this.webCallingService,
187
- {
188
- ...payload.data,
189
- wrapUpRequired:
190
- payload.data.interaction?.participants?.[this.agentId]?.isWrapUp || false,
191
- isConferenceInProgress: getIsConferenceInProgress(payload.data),
192
- isAutoAnswering: shouldAutoAnswer, // Set flag before emitting
193
- },
194
- this.wrapupData,
195
- this.agentId
196
- );
197
- this.taskCollection[payload.data.interactionId] = task;
198
- // Condition 1: The state is=new i.e it is a incoming task
199
- if (payload.data.interaction.state === 'new') {
200
- LoggerProxy.log(
201
- `Got AGENT_CONTACT for a task with state=new, sending TASK_INCOMING event`,
202
- {
203
- module: TASK_MANAGER_FILE,
204
- method: METHODS.REGISTER_TASK_LISTENERS,
205
- interactionId: payload.data.interactionId,
206
- }
207
- );
208
- this.emit(TASK_EVENTS.TASK_INCOMING, task);
209
- } else {
210
- // Condition 2: The state is anything else i.e the task was connected
211
- LoggerProxy.log(
212
- `Got AGENT_CONTACT for a task with state=${payload.data.interaction.state}, sending TASK_HYDRATE event`,
213
- {
214
- module: TASK_MANAGER_FILE,
215
- method: METHODS.REGISTER_TASK_LISTENERS,
216
- interactionId: payload.data.interactionId,
217
- }
218
- );
219
- this.emit(TASK_EVENTS.TASK_HYDRATE, task);
220
- }
221
- }
222
- break;
223
-
224
- case CC_EVENTS.AGENT_CONTACT_RESERVED: {
225
- // Check if auto-answer should happen for this task
226
- const shouldAutoAnswerReserved = shouldAutoAnswerTask(
227
- payload.data,
228
- this.agentId,
229
- this.webCallingService.loginOption,
230
- this.webRtcEnabled
231
- );
232
-
233
- task = new Task(
234
- this.contact,
235
- this.webCallingService,
236
- {
237
- ...payload.data,
238
- isConsulted: false,
239
- isAutoAnswering: shouldAutoAnswerReserved, // Set flag before emitting
240
- },
241
- this.wrapupData,
242
- this.agentId
243
- );
244
- this.taskCollection[payload.data.interactionId] = task;
245
- if (
246
- this.webCallingService.loginOption !== LoginOption.BROWSER ||
247
- task.data.interaction.mediaType !== MEDIA_CHANNEL.TELEPHONY // for digital channels
248
- ) {
249
- this.emit(TASK_EVENTS.TASK_INCOMING, task);
250
- } else if (this.call) {
251
- this.emit(TASK_EVENTS.TASK_INCOMING, task);
252
- }
253
- break;
254
- }
255
- case CC_EVENTS.AGENT_OFFER_CONTACT:
256
- // We don't have to emit any event here since this will be result of promise.
257
- task = this.updateTaskData(task, payload.data);
258
- LoggerProxy.log(`Agent offer contact received for task`, {
259
- module: TASK_MANAGER_FILE,
260
- method: METHODS.REGISTER_TASK_LISTENERS,
261
- interactionId: payload.data?.interactionId,
262
- });
263
- this.emit(TASK_EVENTS.TASK_OFFER_CONTACT, task);
264
-
265
- // Handle auto-answer for offer contact
266
- this.handleAutoAnswer(task);
267
- break;
268
- case CC_EVENTS.AGENT_OUTBOUND_FAILED:
269
- if (task) {
270
- task = this.updateTaskData(task, payload.data);
271
- this.metricsManager.trackEvent(
272
- METRIC_EVENT_NAMES.TASK_OUTDIAL_FAILED,
273
- {
274
- ...MetricsManager.getCommonTrackingFieldForAQMResponse(payload.data),
275
- taskId: payload.data.interactionId,
276
- reason: payload.data.reasonCode || payload.data.reason,
277
- },
278
- ['behavioral', 'operational']
279
- );
280
- LoggerProxy.log(`Agent outbound failed for task`, {
281
- module: TASK_MANAGER_FILE,
282
- method: METHODS.REGISTER_TASK_LISTENERS,
283
- interactionId: payload.data.interactionId,
284
- });
285
- task.emit(TASK_EVENTS.TASK_OUTDIAL_FAILED, payload.data.reason ?? 'UNKNOWN_REASON');
286
- }
287
- break;
288
- case CC_EVENTS.AGENT_CONTACT_ASSIGNED:
289
- // When a campaign preview contact is accepted, the assigned event may arrive
290
- // with a new interactionId while the task is stored under the original
291
- // reservationInteractionId. Fall back to that key so the task is found.
292
- if (!task && payload.data.reservationInteractionId) {
293
- task = this.taskCollection[payload.data.reservationInteractionId];
294
- if (task) {
295
- // Re-key the task under the new interaction ID and remove the old entry
296
- delete this.taskCollection[payload.data.reservationInteractionId];
297
- this.taskCollection[payload.data.interactionId] = task;
298
- }
299
- }
300
- if (task) {
301
- task = this.updateTaskData(task, payload.data);
302
- task.emit(TASK_EVENTS.TASK_ASSIGNED, task);
303
- }
304
- break;
305
- case CC_EVENTS.AGENT_CONTACT_UNASSIGNED:
306
- task = this.updateTaskData(task, {
307
- ...payload.data,
308
- wrapUpRequired: true,
309
- });
310
- task.emit(TASK_EVENTS.TASK_END, task);
311
- break;
312
- case CC_EVENTS.AGENT_CONTACT_OFFER_RONA:
313
- case CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED:
314
- case CC_EVENTS.AGENT_INVITE_FAILED: {
315
- LoggerProxy.warn(
316
- `[DEBUG-CAMPAIGN-CLEAR] Task removal triggered by ${payload.data.type}, interactionId=${payload.data.interactionId}, taskType=${task?.data?.type}`,
317
- {
318
- module: TASK_MANAGER_FILE,
319
- method: METHODS.REGISTER_TASK_LISTENERS,
320
- interactionId: payload.data.interactionId,
321
- }
322
- );
323
- task = this.updateTaskData(task, payload.data);
324
-
325
- const eventTypeToMetricMap: Record<string, keyof typeof METRIC_EVENT_NAMES> = {
326
- [CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED]: 'AGENT_CONTACT_ASSIGN_FAILED',
327
- [CC_EVENTS.AGENT_INVITE_FAILED]: 'AGENT_INVITE_FAILED',
328
- };
329
- const metricEventName: keyof typeof METRIC_EVENT_NAMES =
330
- eventTypeToMetricMap[payload.data.type] || 'AGENT_RONA';
331
-
332
- this.metricsManager.trackEvent(
333
- METRIC_EVENT_NAMES[metricEventName],
334
- {
335
- ...MetricsManager.getCommonTrackingFieldForAQMResponse(payload.data),
336
- taskId: payload.data.interactionId,
337
- reason: payload.data.reason,
338
- },
339
- ['behavioral', 'operational']
340
- );
341
- this.handleTaskCleanup(task);
342
- task.emit(TASK_EVENTS.TASK_REJECT, payload.data.reason);
343
- break;
344
- }
345
- case CC_EVENTS.CONTACT_ENDED:
346
- // Update task data
347
- if (task) {
348
- LoggerProxy.warn(
349
- `[DEBUG-CAMPAIGN-CLEAR] CONTACT_ENDED, interactionId=${payload.data.interactionId}, taskType=${task?.data?.type}, state=${task?.data?.interaction?.state}`,
350
- {
351
- module: TASK_MANAGER_FILE,
352
- method: METHODS.REGISTER_TASK_LISTENERS,
353
- interactionId: payload.data.interactionId,
354
- }
355
- );
356
- task = this.updateTaskData(task, {
357
- ...payload.data,
358
- wrapUpRequired: payload.data.agentsPendingWrapUp?.includes(this.agentId) || false,
359
- });
360
-
361
- // Handle cleanup based on whether task should be deleted
362
- this.handleTaskCleanup(task);
363
-
364
- task?.emit(TASK_EVENTS.TASK_END, task);
365
- }
366
- break;
367
- case CC_EVENTS.CAMPAIGN_CONTACT_UPDATED:
368
- // CampaignContactUpdated is a non-terminal event (intermediate update during accept).
369
- // Only update the task data — do NOT remove the task or emit TASK_END.
370
- // Task cleanup is handled by CONTACT_ENDED or other terminal events.
371
- if (task) {
372
- task = this.updateTaskData(task, payload.data);
373
- }
374
- break;
375
- case CC_EVENTS.CONTACT_MERGED:
376
- task = this.handleContactMerged(task, payload.data);
377
- break;
378
- case CC_EVENTS.AGENT_CONTACT_HELD:
379
- // As soon as the main interaction is held, we need to emit TASK_HOLD
380
- task = this.updateTaskData(task, payload.data);
381
- task.emit(TASK_EVENTS.TASK_HOLD, task);
382
- break;
383
- case CC_EVENTS.AGENT_CONTACT_UNHELD:
384
- // As soon as the main interaction is unheld, we need to emit TASK_RESUME
385
- task = this.updateTaskData(task, payload.data);
386
- task.emit(TASK_EVENTS.TASK_RESUME, task);
387
- break;
388
- case CC_EVENTS.AGENT_VTEAM_TRANSFERRED:
389
- task = this.updateTaskData(task, {
390
- ...payload.data,
391
- wrapUpRequired: true,
392
- });
393
- task.emit(TASK_EVENTS.TASK_END, task);
394
- break;
395
- case CC_EVENTS.AGENT_CTQ_CANCEL_FAILED:
396
- task = this.updateTaskData(task, payload.data);
397
- task.emit(TASK_EVENTS.TASK_CONSULT_QUEUE_FAILED, task);
398
- break;
399
- case CC_EVENTS.AGENT_CONSULT_CREATED:
400
- // Received when self agent initiates a consult
401
- task = this.updateTaskData(task, {
402
- ...payload.data,
403
- isConsulted: false, // This ensures that the task consult status is always reset
404
- });
405
- task.emit(TASK_EVENTS.TASK_CONSULT_CREATED, task);
406
- break;
407
- case CC_EVENTS.AGENT_OFFER_CONSULT:
408
- // Received when other agent sends us a consult offer
409
- task = this.updateTaskData(task, {
410
- ...payload.data,
411
- isConsulted: true, // This ensures that the task is marked as us being requested for a consult
412
- });
413
- task.emit(TASK_EVENTS.TASK_OFFER_CONSULT, task);
414
-
415
- // Handle auto-answer for consult offer
416
- this.handleAutoAnswer(task);
417
- break;
418
- case CC_EVENTS.AGENT_CONSULTING:
419
- // Received when agent is in an active consult state
420
- // TODO: Check if we can use backend consult state instead of isConsulted
421
- task = this.updateTaskData(task, payload.data);
422
- if (task.data.isConsulted) {
423
- // Fire only if you are the agent who received the consult request
424
- task.emit(TASK_EVENTS.TASK_CONSULT_ACCEPTED, task);
425
- } else {
426
- // Fire only if you are the agent who initiated the consult
427
- task.emit(TASK_EVENTS.TASK_CONSULTING, task);
428
- }
429
- break;
430
- case CC_EVENTS.AGENT_CONSULT_FAILED:
431
- // This can only be received by the agent who initiated the consult.
432
- // We need not emit any event here since this will be result of promise
433
- task = this.updateTaskData(task, payload.data);
434
- break;
435
- case CC_EVENTS.AGENT_CONSULT_ENDED:
436
- task = this.updateTaskData(task, payload.data);
437
- if (task.data.isConsulted) {
438
- // This will be the end state of the task as soon as we end the consult in case of
439
- // us being offered a consult
440
- this.removeTaskFromCollection(task);
441
- }
442
- task.emit(TASK_EVENTS.TASK_CONSULT_END, task);
443
- break;
444
- case CC_EVENTS.AGENT_CTQ_CANCELLED:
445
- // This event is received when the consult using queue is cancelled using API
446
- task = this.updateTaskData(task, payload.data);
447
- task.emit(TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED, task);
448
- break;
449
- case CC_EVENTS.AGENT_WRAPUP:
450
- task = this.updateTaskData(task, {...payload.data, wrapUpRequired: true});
451
- task.emit(TASK_EVENTS.TASK_END, task);
452
- break;
453
- case CC_EVENTS.AGENT_WRAPPEDUP:
454
- task.cancelAutoWrapupTimer();
455
- this.removeTaskFromCollection(task);
456
- task.emit(TASK_EVENTS.TASK_WRAPPEDUP, task);
457
- break;
458
- case CC_EVENTS.CONTACT_RECORDING_PAUSED:
459
- task = this.updateTaskData(task, payload.data);
460
- task.emit(TASK_EVENTS.TASK_RECORDING_PAUSED, task);
461
- break;
462
- case CC_EVENTS.CONTACT_RECORDING_PAUSE_FAILED:
463
- task = this.updateTaskData(task, payload.data);
464
- task.emit(TASK_EVENTS.TASK_RECORDING_PAUSE_FAILED, task);
465
- break;
466
- case CC_EVENTS.CONTACT_RECORDING_RESUMED:
467
- task = this.updateTaskData(task, payload.data);
468
- task.emit(TASK_EVENTS.TASK_RECORDING_RESUMED, task);
469
- break;
470
- case CC_EVENTS.CONTACT_RECORDING_RESUME_FAILED:
471
- task = this.updateTaskData(task, payload.data);
472
- task.emit(TASK_EVENTS.TASK_RECORDING_RESUME_FAILED, task);
473
- break;
474
- case CC_EVENTS.AGENT_CONSULT_CONFERENCING:
475
- // Conference is being established - update task state and emit establishing event
476
- task = this.updateTaskData(task, payload.data);
477
- task.emit(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, task);
478
- break;
479
- case CC_EVENTS.AGENT_CONSULT_CONFERENCED:
480
- // Conference started successfully - update task state and emit event
481
- task = this.updateTaskData(task, payload.data);
482
- task.emit(TASK_EVENTS.TASK_CONFERENCE_STARTED, task);
483
- break;
484
- case CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED:
485
- // Conference failed - update task state and emit failure event
486
- task = this.updateTaskData(task, payload.data);
487
- task.emit(TASK_EVENTS.TASK_CONFERENCE_FAILED, task);
488
- break;
489
- case CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED:
490
- // Conference ended - update task state and emit event
491
- task = this.updateTaskData(task, payload.data);
492
- if (
493
- !task ||
494
- isPrimary(task, this.agentId) ||
495
- isParticipantInMainInteraction(task, this.agentId)
496
- ) {
497
- LoggerProxy.log('Primary or main interaction participant leaving conference');
498
- } else {
499
- this.removeTaskFromCollection(task);
500
- }
501
- task.emit(TASK_EVENTS.TASK_CONFERENCE_ENDED, task);
502
- break;
503
- case CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE: {
504
- task = this.updateTaskData(task, {
505
- ...payload.data,
506
- isConferenceInProgress: getIsConferenceInProgress(payload.data),
507
- });
508
- task.emit(TASK_EVENTS.TASK_PARTICIPANT_JOINED, task);
509
- break;
510
- }
511
- case CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE: {
512
- // Conference ended - update task state and emit event
513
-
514
- task = this.updateTaskData(task, {
515
- ...payload.data,
516
- isConferenceInProgress: getIsConferenceInProgress(payload.data),
517
- });
518
- if (checkParticipantNotInInteraction(task, this.agentId)) {
519
- if (
520
- isParticipantInMainInteraction(task, this.agentId) ||
521
- isPrimary(task, this.agentId)
522
- ) {
523
- LoggerProxy.log('Primary or main interaction participant leaving conference');
524
- } else {
525
- this.removeTaskFromCollection(task);
526
- }
527
- }
528
- task.emit(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
529
- break;
530
- }
531
- case CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE_FAILED:
532
- // Conference exit failed - update task state and emit failure event
533
- task = this.updateTaskData(task, payload.data);
534
- task.emit(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, task);
535
- break;
536
- case CC_EVENTS.AGENT_CONSULT_CONFERENCE_END_FAILED:
537
- // Conference end failed - update task state with error details and emit failure event
538
- task = this.updateTaskData(task, payload.data);
539
- task.emit(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, task);
540
- break;
541
- case CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED:
542
- // Conference was transferred - update task state and emit transfer success event
543
- // Note: Backend should provide hasLeft and wrapUpRequired status
544
- task = this.updateTaskData(task, payload.data);
545
- task.emit(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, task);
546
- break;
547
- case CC_EVENTS.AGENT_CONFERENCE_TRANSFER_FAILED:
548
- // Conference transfer failed - update task state with error details and emit failure event
549
- task = this.updateTaskData(task, payload.data);
550
- task.emit(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, task);
551
- break;
552
- case CC_EVENTS.PARTICIPANT_POST_CALL_ACTIVITY:
553
- // Post-call activity for participant - update task state with activity details
554
- task = this.updateTaskData(task, payload.data);
555
- task.emit(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, task);
556
- break;
557
- case CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION: {
558
- // Campaign preview contact offered to agent
559
- // Create a task in the collection so subsequent events (e.g. AGENT_CONTACT_ASSIGNED
560
- // after acceptPreviewContact) can find and update it.
561
- // Emit TASK_CAMPAIGN_PREVIEW_RESERVATION instead of TASK_INCOMING so the call
562
- // does not ring out to the customer before the agent explicitly accepts the preview contact.
563
- LoggerProxy.log('Campaign preview reservation received', {
564
- module: TASK_MANAGER_FILE,
565
- method: METHODS.REGISTER_TASK_LISTENERS,
566
- interactionId: payload.data.interactionId,
567
- });
568
-
569
- if (!this.taskCollection[payload.data.interactionId]) {
570
- task = new Task(
571
- this.contact,
572
- this.webCallingService,
573
- {
574
- ...payload.data,
575
- wrapUpRequired: false,
576
- isConferenceInProgress: false,
577
- isAutoAnswering: false,
578
- },
579
- this.wrapupData,
580
- this.agentId
581
- );
582
- this.taskCollection[payload.data.interactionId] = task;
583
- } else {
584
- task = this.updateTaskData(task, payload.data);
585
- }
586
-
587
- this.emit(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION, task);
588
- break;
589
- }
351
+ // Step 1: Parse and validate the message
352
+ const message = TaskManager.parseWebSocketMessage(event);
353
+ if (!message) return;
590
354
 
591
- default:
592
- break;
593
- }
594
- if (task) {
595
- task.emit(payload.data.type, payload.data);
596
- }
355
+ // Step 2: Prepare event context
356
+ const eventContext = this.prepareEventContext(message);
357
+ if (!eventContext) return;
597
358
 
598
- const transcriptInteractionId =
599
- payload.data?.interactionId ||
600
- payload.data?.data?.conversationId ||
601
- task?.data?.interactionId;
359
+ const actions = this.handleTaskLifecycleEvent(eventContext);
602
360
 
603
- if (TRANSCRIPT_EVENT_MAP[payload.data.type] && transcriptInteractionId) {
604
- this.requestRealTimeTranscripts(payload.data.type, transcriptInteractionId);
605
- }
361
+ const {task} = actions;
362
+ if (!task) return;
363
+
364
+ const {payload, stateMachineEvent} = eventContext;
365
+
366
+ // Always keep task.data updated (even for mapped events) so consumers relying
367
+ // on TaskManager-managed task instances see the latest payload.
368
+ if (payload) {
369
+ this.updateTaskData(task, payload);
606
370
  }
371
+
372
+ // Send event to state machine - this will trigger all TASK_EVENTS emissions
373
+ // including TASK_INCOMING which is now handled via the state machine callbacks
374
+ if (stateMachineEvent) {
375
+ task.sendStateMachineEvent(stateMachineEvent);
376
+ }
377
+
378
+ // Send transcript start/stop events for relevant CC events
379
+ this.requestRealTimeTranscripts(eventContext.eventType, payload.interactionId);
607
380
  });
608
381
  }
609
382
 
610
- private updateTaskData(task: ITask, taskData: TaskData): ITask {
611
- if (!task) {
612
- return undefined;
613
- }
383
+ /**
384
+ * Parse and validate WebSocket message
385
+ * @returns Parsed message or null if invalid/keepalive
386
+ */
387
+ private static parseWebSocketMessage(event: string): WebSocketMessage | null {
388
+ try {
389
+ const payload = JSON.parse(event) as WebSocketMessage;
614
390
 
615
- if (!taskData?.interactionId) {
616
- LoggerProxy.warn('Received task update with missing interactionId', {
617
- module: TASK_MANAGER_FILE,
618
- method: METHODS.UPDATE_TASK_DATA,
619
- });
620
- }
391
+ // Filter out keepalive messages
392
+ if (payload?.keepalive === 'true' || payload?.keepalive === true) {
393
+ return null;
394
+ }
621
395
 
622
- try {
623
- const currentTask = task.updateTaskData(taskData);
624
- this.taskCollection[taskData.interactionId] = currentTask;
396
+ // Normalize task data if present
397
+ if (payload?.data?.interaction) {
398
+ payload.data = normalizeTaskData(payload.data);
399
+ }
625
400
 
626
- return currentTask;
401
+ return payload;
627
402
  } catch (error) {
628
- LoggerProxy.error(`Failed to update task`, {
403
+ LoggerProxy.error('Failed to parse WebSocket message', {
629
404
  module: TASK_MANAGER_FILE,
630
- method: METHODS.UPDATE_TASK_DATA,
631
- interactionId: taskData.interactionId,
405
+ method: 'parseWebSocketMessage',
406
+ error,
632
407
  });
633
408
 
634
- return task;
409
+ return null;
635
410
  }
636
411
  }
637
412
 
638
413
  /**
639
- * Handles CONTACT_MERGED event logic
640
- * @param task - The task to process
641
- * @param taskData - The task data from the event payload
642
- * @returns Updated or newly created task
643
- * @private
414
+ * Prepare context for event processing
415
+ * @returns Event context or null if event type is invalid
644
416
  */
645
- private handleContactMerged(task: ITask, taskData: TaskData): ITask {
646
- if (taskData.childInteractionId) {
647
- // remove the child task from collection
648
- this.removeTaskFromCollection(this.taskCollection[taskData.childInteractionId]);
417
+ private prepareEventContext(message: WebSocketMessage): EventContext | null {
418
+ const eventType = message.data?.type || message.type;
419
+
420
+ if (!eventType || !isCcEvent(eventType)) {
421
+ return null;
649
422
  }
650
423
 
651
- if (this.taskCollection[taskData.interactionId]) {
652
- LoggerProxy.log(`Got CONTACT_MERGED: Task already exists in collection`, {
653
- module: TASK_MANAGER_FILE,
654
- method: METHODS.REGISTER_TASK_LISTENERS,
655
- interactionId: taskData.interactionId,
656
- });
657
- // update the task data
658
- task = this.updateTaskData(task, taskData);
659
- } else {
660
- // Case2 : Task is not present in taskCollection
661
- LoggerProxy.log(`Got CONTACT_MERGED : Creating new task in taskManager`, {
662
- module: TASK_MANAGER_FILE,
663
- method: METHODS.REGISTER_TASK_LISTENERS,
664
- interactionId: taskData.interactionId,
665
- });
424
+ const interactionId = message.data.interactionId;
425
+ const task = this.taskCollection[interactionId];
426
+
427
+ const wasConsultedTask = Boolean(task?.data?.isConsulted);
428
+ const computeWrapUpRequired = () => {
429
+ if (message.data.wrapUpRequired !== undefined) {
430
+ return message.data.wrapUpRequired;
431
+ }
432
+ if (message.data.isConsulted !== undefined) {
433
+ return !message.data.isConsulted;
434
+ }
435
+
436
+ return !wasConsultedTask;
437
+ };
438
+
439
+ const adjustedPayload =
440
+ eventType === CC_EVENTS.AGENT_CONSULT_TRANSFERRED ||
441
+ eventType === CC_EVENTS.AGENT_BLIND_TRANSFERRED ||
442
+ eventType === CC_EVENTS.AGENT_VTEAM_TRANSFERRED
443
+ ? {
444
+ ...message.data,
445
+ wrapUpRequired: computeWrapUpRequired(),
446
+ }
447
+ : message.data;
448
+
449
+ const stateMachineEvent = TaskManager.mapEventToTaskStateMachineEvent(
450
+ eventType,
451
+ adjustedPayload,
452
+ this.agentId
453
+ );
454
+
455
+ LoggerProxy.info(`Handling task event ${eventType}`, {
456
+ module: TASK_MANAGER_FILE,
457
+ method: 'prepareEventContext',
458
+ interactionId,
459
+ });
460
+
461
+ return {
462
+ eventType,
463
+ payload: adjustedPayload,
464
+ task,
465
+ stateMachineEvent,
466
+ };
467
+ }
468
+
469
+ /**
470
+ * Handle task lifecycle events and determine required actions
471
+ *
472
+ * Delegates to specific event handlers based on event type. Each handler
473
+ * is responsible for TaskManager-level concerns:
474
+ * - Task creation and collection management
475
+ * - Metrics tracking
476
+ * - Resource cleanup decisions
477
+ *
478
+ * Note: Task-level state transitions and event emissions are handled by
479
+ * the task state machine via sendStateMachineEvent()
480
+ */
481
+ private handleTaskLifecycleEvent(context: EventContext): TaskEventActions {
482
+ const {eventType} = context;
483
+
484
+ switch (eventType) {
485
+ case CC_EVENTS.AGENT_CONTACT_RESERVED:
486
+ return this.handleContactReserved(context);
487
+
488
+ case CC_EVENTS.AGENT_CONTACT:
489
+ return this.handleAgentContact(context);
490
+
491
+ case CC_EVENTS.CONTACT_MERGED:
492
+ return this.handleContactMergedEvent(context);
493
+
494
+ default:
495
+ return {task: context.task};
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Handle AGENT_CONTACT_RESERVED event
501
+ * Creates a new task; state machine event is sent during processing
502
+ */
503
+ private handleContactReserved(context: EventContext): TaskEventActions {
504
+ const {payload} = context;
505
+ const isConsultedTask =
506
+ payload.isConsulted === true || isSecondaryEpDnAgent(payload.interaction);
507
+ const shouldAutoAnswer = shouldAutoAnswerTask(
508
+ payload,
509
+ this.agentId,
510
+ this.webCallingService.loginOption,
511
+ this.webRtcEnabled
512
+ );
513
+
514
+ const taskData: TaskData = {
515
+ ...payload,
516
+ isConsulted: isConsultedTask,
517
+ isAutoAnswering: shouldAutoAnswer,
518
+ };
519
+
520
+ const task = TaskFactory.createTask(
521
+ this.contact,
522
+ this.webCallingService,
523
+ taskData,
524
+ this.configFlags,
525
+ this.wrapupData,
526
+ this.agentId
527
+ );
528
+
529
+ this.setupTaskListeners(task);
530
+ this.taskCollection[payload.interactionId] = task;
531
+
532
+ return {task};
533
+ }
534
+
535
+ /**
536
+ * Handle AGENT_CONTACT event
537
+ * Re-creates task if missing (multi-session scenario)
538
+ */
539
+ private handleAgentContact(context: EventContext): TaskEventActions {
540
+ let {task} = context;
541
+ const {payload} = context;
666
542
 
667
- task = new Task(
543
+ if (!task) {
544
+ const isConsultedTask =
545
+ payload.isConsulted === true || isSecondaryEpDnAgent(payload.interaction);
546
+ const shouldAutoAnswer = shouldAutoAnswerTask(
547
+ payload,
548
+ this.agentId,
549
+ this.webCallingService.loginOption,
550
+ this.webRtcEnabled
551
+ );
552
+ const taskData: TaskData = {
553
+ ...payload,
554
+ isConsulted: isConsultedTask,
555
+ wrapUpRequired: payload.interaction?.participants?.[this.agentId]?.isWrapUp || false,
556
+ isConferenceInProgress: getIsConferenceInProgress(payload),
557
+ isAutoAnswering: shouldAutoAnswer,
558
+ };
559
+
560
+ task = TaskFactory.createTask(
668
561
  this.contact,
669
562
  this.webCallingService,
670
- {
671
- ...taskData,
672
- wrapUpRequired: taskData.interaction?.participants?.[this.agentId]?.isWrapUp || false,
673
- isConferenceInProgress: getIsConferenceInProgress(taskData),
674
- },
563
+ taskData,
564
+ this.configFlags,
675
565
  this.wrapupData,
676
566
  this.agentId
677
567
  );
678
- this.taskCollection[taskData.interactionId] = task;
568
+ this.setupTaskListeners(task);
569
+ this.taskCollection[payload.interactionId] = task;
570
+ }
571
+
572
+ return {task};
573
+ }
574
+
575
+ private updateTaskData(task: ITask, taskData: TaskData): ITask {
576
+ if (!task) {
577
+ throw new Error('Task not found for update');
679
578
  }
680
579
 
681
- this.emit(TASK_EVENTS.TASK_MERGED, task);
580
+ const snapshot = task.stateMachineService?.getSnapshot?.();
581
+ const isConsultingFlow =
582
+ snapshot?.value === 'CONSULTING' || taskData.interaction?.state === 'consulting';
583
+
584
+ const updateTaskData = isConsultingFlow
585
+ ? {
586
+ ...taskData,
587
+ destAgentId: taskData.destAgentId ?? snapshot?.context?.consultDestinationAgentId ?? null,
588
+ destinationType:
589
+ taskData.destinationType ?? snapshot?.context?.consultDestinationType ?? null,
590
+ }
591
+ : taskData;
592
+
593
+ task.updateTaskData(updateTaskData);
594
+ this.taskCollection[taskData.interactionId] = task;
682
595
 
683
596
  return task;
684
597
  }
685
598
 
599
+ /**
600
+ * Setup listeners for task events that need to be bubbled up to TaskManager
601
+ * This replaces the previous callback injection pattern
602
+ */
603
+ private setupTaskListeners(task: ITask): void {
604
+ // Listen for TASK_INCOMING and re-emit so webex.cc can notify consumers
605
+ task.on(TASK_EVENTS.TASK_INCOMING, (t: ITask) => {
606
+ LoggerProxy.log(`Task incoming event received`, {
607
+ module: TASK_MANAGER_FILE,
608
+ method: METHODS.REGISTER_TASK_LISTENERS,
609
+ interactionId: t.data?.interactionId,
610
+ });
611
+
612
+ this.emit(TASK_EVENTS.TASK_INCOMING, t);
613
+ });
614
+
615
+ // Listen for TASK_HYDRATE on the task and re-emit on TaskManager
616
+ task.on(TASK_EVENTS.TASK_HYDRATE, (t: ITask) => {
617
+ // Task data is already updated by the task itself before emitting
618
+ this.emit(TASK_EVENTS.TASK_HYDRATE, t);
619
+ });
620
+
621
+ // Listen for internal cleanup signal emitted by the state machine
622
+ task.on(TASK_EVENTS.TASK_CLEANUP, (t: ITask, options?: {removeFromCollection?: boolean}) => {
623
+ this.handleTaskCleanup(t);
624
+ if (options?.removeFromCollection) {
625
+ const interactionId = t?.data?.interactionId;
626
+ if (interactionId && this.taskCollection[interactionId]) {
627
+ this.removeTaskFromCollection(t);
628
+ }
629
+ }
630
+ });
631
+ }
632
+
686
633
  private removeTaskFromCollection(task: ITask) {
634
+ if (typeof task.cancelAutoWrapupTimer === 'function') {
635
+ task.cancelAutoWrapupTimer();
636
+ }
687
637
  if (task?.data?.interactionId) {
688
638
  delete this.taskCollection[task.data.interactionId];
689
639
  LoggerProxy.info(`Task removed from collection`, {
@@ -695,69 +645,61 @@ export default class TaskManager extends EventEmitter {
695
645
  }
696
646
 
697
647
  /**
698
- * Handles auto-answer logic for incoming tasks
699
- * Automatically accepts tasks when isAutoAnswering flag is set
700
- * The flag is set during task creation based on:
701
- * 1. WebRTC calls with auto-answer enabled in agent profile
702
- * 2. Agent-initiated WebRTC outdial calls
703
- * 3. Agent-initiated digital outbound (Email/SMS) without previous transfers
704
- *
705
- * @param task - The task to auto-answer
648
+ * Handles CONTACT_MERGED event logic
649
+ * @param task - The task to process
650
+ * @param taskData - The task data from the event payload
651
+ * @returns Updated or newly created task
706
652
  * @private
707
653
  */
708
- private async handleAutoAnswer(task: ITask): Promise<void> {
709
- if (!task || !task.data || !task.data.isAutoAnswering) {
710
- return;
711
- }
654
+ private handleContactMergedEvent(context: EventContext): TaskEventActions {
655
+ const {payload} = context;
656
+ let task = context.task;
712
657
 
713
- LoggerProxy.info(`Auto-answering task`, {
714
- module: TASK_MANAGER_FILE,
715
- method: 'handleAutoAnswer',
716
- interactionId: task.data.interactionId,
717
- });
658
+ if (payload.childInteractionId) {
659
+ // remove the child task from collection
660
+ this.removeTaskFromCollection(this.taskCollection[payload.childInteractionId]);
661
+ }
718
662
 
719
- try {
720
- await task.accept();
721
- LoggerProxy.info(`Task auto-answered successfully`, {
663
+ if (task) {
664
+ LoggerProxy.log(`Got CONTACT_MERGED: Task already exists in collection`, {
722
665
  module: TASK_MANAGER_FILE,
723
- method: 'handleAutoAnswer',
724
- interactionId: task.data.interactionId,
666
+ method: METHODS.REGISTER_TASK_LISTENERS,
667
+ interactionId: payload.interactionId,
725
668
  });
726
-
727
- // Track successful auto-answer
728
- this.metricsManager.trackEvent(
729
- METRIC_EVENT_NAMES.TASK_AUTO_ANSWER_SUCCESS,
730
- {
731
- taskId: task.data.interactionId,
732
- mediaType: task.data.interaction.mediaType,
733
- isAutoAnswered: true,
734
- },
735
- ['behavioral', 'operational']
736
- );
737
- // Emit task:autoAnswered event for widgets/UI to react
738
- task.emit(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
739
- } catch (error) {
740
- // Reset isAutoAnswering flag on failure
741
- task.updateTaskData({...task.data, isAutoAnswering: false});
742
- LoggerProxy.error(`Failed to auto-answer task`, {
669
+ // update the task data
670
+ this.updateTaskData(task, payload);
671
+ } else {
672
+ // Case2 : Task is not present in taskCollection
673
+ LoggerProxy.log(`Got CONTACT_MERGED : Creating new task in taskManager`, {
743
674
  module: TASK_MANAGER_FILE,
744
- method: 'handleAutoAnswer',
745
- interactionId: task.data.interactionId,
746
- error,
675
+ method: METHODS.REGISTER_TASK_LISTENERS,
676
+ interactionId: payload.interactionId,
747
677
  });
748
678
 
749
- // Track auto-answer failure
750
- this.metricsManager.trackEvent(
751
- METRIC_EVENT_NAMES.TASK_AUTO_ANSWER_FAILED,
752
- {
753
- taskId: task.data.interactionId,
754
- mediaType: task.data.interaction.mediaType,
755
- error: error?.message || 'Unknown error',
756
- isAutoAnswered: false,
757
- },
758
- ['behavioral', 'operational']
679
+ const taskData: TaskData = {
680
+ ...payload,
681
+ wrapUpRequired: payload.interaction?.participants?.[this.agentId]?.isWrapUp || false,
682
+ isConferenceInProgress: getIsConferenceInProgress(payload),
683
+ isConsulted: false,
684
+ };
685
+
686
+ task = TaskFactory.createTask(
687
+ this.contact,
688
+ this.webCallingService,
689
+ taskData,
690
+ this.configFlags,
691
+ this.wrapupData,
692
+ this.agentId
759
693
  );
694
+ this.setupTaskListeners(task);
695
+ this.taskCollection[payload.interactionId] = task;
760
696
  }
697
+
698
+ if (task) {
699
+ this.emit(TASK_EVENTS.TASK_MERGED, task);
700
+ }
701
+
702
+ return {task};
761
703
  }
762
704
 
763
705
  /**
@@ -766,10 +708,10 @@ export default class TaskManager extends EventEmitter {
766
708
  * @private
767
709
  */
768
710
  private handleTaskCleanup(task: ITask) {
769
- // Clean up Desktop/WebRTC calling resources for browser-based telephony tasks
770
711
  if (
771
712
  this.webCallingService.loginOption === LoginOption.BROWSER &&
772
- task.data.interaction.mediaType === 'telephony'
713
+ task.data.interaction.mediaType === MEDIA_CHANNEL.TELEPHONY &&
714
+ task instanceof WebRTC
773
715
  ) {
774
716
  task.unregisterWebCallListeners();
775
717
  this.webCallingService.cleanUpCall();
@@ -777,16 +719,13 @@ export default class TaskManager extends EventEmitter {
777
719
 
778
720
  const isOutdial = task.data.interaction.outboundType === 'OUTDIAL';
779
721
  const isNew = task.data.interaction.state === 'new';
780
- const needsWrapUp = task.data.agentsPendingWrapUp?.includes(this.agentId) ?? false;
722
+ const needsWrapUp = task.data.agentsPendingWrapUp?.length > 0;
781
723
 
782
724
  // For OUTDIAL: only remove if NOT terminated (user-declined, no wrap-up follows)
725
+ // If terminated, keep task for wrap-up flow (CONTACT_ENDED → AGENT_WRAPUP)
783
726
  // For non-OUTDIAL: remove if state is 'new'
784
727
  // Always remove if secondary EpDn agent
785
- if (
786
- (isNew && !(isOutdial && needsWrapUp)) ||
787
- isSecondaryEpDnAgent(task.data.interaction) ||
788
- (!needsWrapUp && isOutdial) // For outdial tasks, needs wrap-up is false and state is "WRAPUP". We need to just remove the task.
789
- ) {
728
+ if ((isNew && !(isOutdial && needsWrapUp)) || isSecondaryEpDnAgent(task.data.interaction)) {
790
729
  this.removeTaskFromCollection(task);
791
730
  }
792
731
  }
@@ -797,12 +736,8 @@ export default class TaskManager extends EventEmitter {
797
736
  */
798
737
  private requestRealTimeTranscripts(eventType: string, interactionId: string): void {
799
738
  const action = TRANSCRIPT_EVENT_MAP[eventType];
800
- if (
801
- !action ||
802
- !this.apiAIAssistant ||
803
- this.apiAIAssistant.aiFeature?.realtimeTranscripts?.enable === false
804
- )
805
- return;
739
+ if (!action || !this.apiAIAssistant) return;
740
+ if (this.configFlags?.aiFeature?.realtimeTranscripts?.enable === false) return;
806
741
 
807
742
  this.apiAIAssistant
808
743
  .sendEvent(
@@ -815,7 +750,7 @@ export default class TaskManager extends EventEmitter {
815
750
  .catch((error) => {
816
751
  LoggerProxy.error(`Failed to send transcript ${action} event`, {
817
752
  module: TASK_MANAGER_FILE,
818
- method: 'requestRealTimeTranscripts',
753
+ method: METHODS.REQUEST_REAL_TIME_TRANSCRIPTS,
819
754
  interactionId,
820
755
  error,
821
756
  });
@@ -826,28 +761,27 @@ export default class TaskManager extends EventEmitter {
826
761
  return this.taskCollection[taskId];
827
762
  }
828
763
 
829
- /**
830
- * @param taskId - Unique identifier for each task
831
- */
832
- public getAllTasks = (): Record<TaskId, ITask> => {
833
- return this.taskCollection;
834
- };
764
+ public getAllTasks(): Record<TaskId, ITask> {
765
+ return {...this.taskCollection};
766
+ }
835
767
 
836
768
  public static getTaskManager(
837
769
  apiAIAssistant: ApiAIAssistant,
838
770
  contact: ReturnType<typeof routingContact>,
839
771
  webCallingService: WebCallingService,
840
- webSocketManager: WebSocketManager
772
+ webSocketManager: WebSocketManager,
773
+ rtdWebSocketManager?: WebSocketManager
841
774
  ): TaskManager {
842
775
  if (!TaskManager.taskManager) {
843
776
  TaskManager.taskManager = new TaskManager(
844
777
  apiAIAssistant,
845
778
  contact,
846
779
  webCallingService,
847
- webSocketManager
780
+ webSocketManager,
781
+ rtdWebSocketManager
848
782
  );
849
783
  }
850
784
 
851
- return this.taskManager;
785
+ return TaskManager.taskManager;
852
786
  }
853
787
  }