@webex/contact-center 3.11.0 → 3.12.0-llmrefactor.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 (267) hide show
  1. package/.sdd/manifest.json +882 -0
  2. package/AGENTS.md +94 -0
  3. package/ai-docs/ARCHITECTURE.md +168 -0
  4. package/ai-docs/CONTRACTS.md +46 -0
  5. package/ai-docs/GETTING_STARTED.md +168 -0
  6. package/ai-docs/GLOSSARY.md +43 -0
  7. package/ai-docs/README.md +138 -0
  8. package/ai-docs/REVIEW_CHECKLIST.md +41 -0
  9. package/ai-docs/RULES.md +444 -0
  10. package/ai-docs/SECURITY.md +52 -0
  11. package/ai-docs/SERVICE_STATE.md +48 -0
  12. package/ai-docs/SPEC_INDEX.md +65 -0
  13. package/ai-docs/adr/0001-spec-source-policy.md +55 -0
  14. package/ai-docs/adr/README.md +8 -0
  15. package/ai-docs/adr/_adr-template.md +31 -0
  16. package/ai-docs/contact-center-spec.md +341 -0
  17. package/ai-docs/features/generated-spec-conformance-fidelity-remediation/spec/feature-spec.md +117 -0
  18. package/ai-docs/features/residual-warning-coverage-completion/spec/feature-spec.md +203 -0
  19. package/ai-docs/features/validator-code-fidelity-drift-fix/spec/feature-spec.md +315 -0
  20. package/ai-docs/patterns/event-driven-patterns.md +485 -0
  21. package/ai-docs/patterns/testing-patterns.md +480 -0
  22. package/ai-docs/patterns/typescript-patterns.md +365 -0
  23. package/ai-docs/templates/README.md +102 -0
  24. package/ai-docs/templates/documentation/create-agents-md.md +240 -0
  25. package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
  26. package/ai-docs/templates/existing-service/bug-fix.md +254 -0
  27. package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
  28. package/ai-docs/templates/new-method/00-master.md +80 -0
  29. package/ai-docs/templates/new-method/01-requirements.md +232 -0
  30. package/ai-docs/templates/new-method/02-implementation.md +295 -0
  31. package/ai-docs/templates/new-method/03-tests.md +201 -0
  32. package/ai-docs/templates/new-method/04-validation.md +141 -0
  33. package/ai-docs/templates/new-service/00-master.md +109 -0
  34. package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
  35. package/ai-docs/templates/new-service/02-code-generation.md +346 -0
  36. package/ai-docs/templates/new-service/03-integration.md +178 -0
  37. package/ai-docs/templates/new-service/04-test-generation.md +205 -0
  38. package/ai-docs/templates/new-service/05-validation.md +145 -0
  39. package/dist/cc.js +379 -50
  40. package/dist/cc.js.map +1 -1
  41. package/dist/config.js +6 -0
  42. package/dist/config.js.map +1 -1
  43. package/dist/constants.js +22 -2
  44. package/dist/constants.js.map +1 -1
  45. package/dist/index.js +27 -5
  46. package/dist/index.js.map +1 -1
  47. package/dist/metrics/behavioral-events.js +114 -0
  48. package/dist/metrics/behavioral-events.js.map +1 -1
  49. package/dist/metrics/constants.js +32 -3
  50. package/dist/metrics/constants.js.map +1 -1
  51. package/dist/services/ApiAiAssistant.js +318 -0
  52. package/dist/services/ApiAiAssistant.js.map +1 -0
  53. package/dist/services/UserPreference.js +427 -0
  54. package/dist/services/UserPreference.js.map +1 -0
  55. package/dist/services/agent/types.js.map +1 -1
  56. package/dist/services/config/Util.js +8 -4
  57. package/dist/services/config/Util.js.map +1 -1
  58. package/dist/services/config/constants.js +35 -2
  59. package/dist/services/config/constants.js.map +1 -1
  60. package/dist/services/config/index.js +41 -2
  61. package/dist/services/config/index.js.map +1 -1
  62. package/dist/services/config/types.js +66 -8
  63. package/dist/services/config/types.js.map +1 -1
  64. package/dist/services/constants.js +27 -1
  65. package/dist/services/constants.js.map +1 -1
  66. package/dist/services/core/Err.js.map +1 -1
  67. package/dist/services/core/Utils.js +122 -25
  68. package/dist/services/core/Utils.js.map +1 -1
  69. package/dist/services/core/aqm-reqs.js +92 -17
  70. package/dist/services/core/aqm-reqs.js.map +1 -1
  71. package/dist/services/core/websocket/WebSocketManager.js +22 -6
  72. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  73. package/dist/services/core/websocket/connection-service.js +3 -1
  74. package/dist/services/core/websocket/connection-service.js.map +1 -1
  75. package/dist/services/core/websocket/types.js.map +1 -1
  76. package/dist/services/index.js +6 -0
  77. package/dist/services/index.js.map +1 -1
  78. package/dist/services/task/Task.js +688 -0
  79. package/dist/services/task/Task.js.map +1 -0
  80. package/dist/services/task/TaskFactory.js +45 -0
  81. package/dist/services/task/TaskFactory.js.map +1 -0
  82. package/dist/services/task/TaskManager.js +751 -457
  83. package/dist/services/task/TaskManager.js.map +1 -1
  84. package/dist/services/task/TaskUtils.js +220 -23
  85. package/dist/services/task/TaskUtils.js.map +1 -1
  86. package/dist/services/task/constants.js +23 -2
  87. package/dist/services/task/constants.js.map +1 -1
  88. package/dist/services/task/dialer.js +129 -0
  89. package/dist/services/task/dialer.js.map +1 -1
  90. package/dist/services/task/digital/Digital.js +77 -0
  91. package/dist/services/task/digital/Digital.js.map +1 -0
  92. package/dist/services/task/state-machine/TaskStateMachine.js +873 -0
  93. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
  94. package/dist/services/task/state-machine/actions.js +567 -0
  95. package/dist/services/task/state-machine/actions.js.map +1 -0
  96. package/dist/services/task/state-machine/constants.js +161 -0
  97. package/dist/services/task/state-machine/constants.js.map +1 -0
  98. package/dist/services/task/state-machine/guards.js +382 -0
  99. package/dist/services/task/state-machine/guards.js.map +1 -0
  100. package/dist/services/task/state-machine/index.js +53 -0
  101. package/dist/services/task/state-machine/index.js.map +1 -0
  102. package/dist/services/task/state-machine/types.js +54 -0
  103. package/dist/services/task/state-machine/types.js.map +1 -0
  104. package/dist/services/task/state-machine/uiControlsComputer.js +603 -0
  105. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
  106. package/dist/services/task/taskDataNormalizer.js +99 -0
  107. package/dist/services/task/taskDataNormalizer.js.map +1 -0
  108. package/dist/services/task/types.js +227 -4
  109. package/dist/services/task/types.js.map +1 -1
  110. package/dist/services/task/voice/Voice.js +1044 -0
  111. package/dist/services/task/voice/Voice.js.map +1 -0
  112. package/dist/services/task/voice/WebRTC.js +149 -0
  113. package/dist/services/task/voice/WebRTC.js.map +1 -0
  114. package/dist/types/cc.d.ts +894 -0
  115. package/dist/types/config.d.ts +72 -0
  116. package/dist/types/constants.d.ts +66 -0
  117. package/dist/types/index.d.ts +199 -0
  118. package/dist/types/logger-proxy.d.ts +71 -0
  119. package/dist/types/metrics/MetricsManager.d.ts +223 -0
  120. package/dist/types/metrics/behavioral-events.d.ts +29 -0
  121. package/dist/types/metrics/constants.d.ts +181 -0
  122. package/dist/types/services/AddressBook.d.ts +74 -0
  123. package/dist/types/services/ApiAiAssistant.d.ts +49 -0
  124. package/dist/types/services/EntryPoint.d.ts +67 -0
  125. package/dist/types/services/Queue.d.ts +76 -0
  126. package/dist/types/services/UserPreference.d.ts +118 -0
  127. package/dist/types/services/WebCallingService.d.ts +1 -0
  128. package/dist/types/services/agent/index.d.ts +46 -0
  129. package/dist/types/services/agent/types.d.ts +413 -0
  130. package/dist/types/services/config/Util.d.ts +20 -0
  131. package/dist/types/services/config/constants.d.ts +270 -0
  132. package/dist/types/services/config/index.d.ts +177 -0
  133. package/dist/types/services/config/types.d.ts +1368 -0
  134. package/dist/types/services/constants.d.ts +110 -0
  135. package/dist/types/services/core/Err.d.ts +125 -0
  136. package/dist/types/services/core/GlobalTypes.d.ts +58 -0
  137. package/dist/types/services/core/Utils.d.ts +121 -0
  138. package/dist/types/services/core/WebexRequest.d.ts +22 -0
  139. package/dist/types/services/core/aqm-reqs.d.ts +65 -0
  140. package/dist/types/services/core/constants.d.ts +99 -0
  141. package/dist/types/services/core/types.d.ts +47 -0
  142. package/dist/types/services/core/websocket/WebSocketManager.d.ts +36 -0
  143. package/dist/types/services/core/websocket/connection-service.d.ts +27 -0
  144. package/dist/types/services/core/websocket/keepalive.worker.d.ts +2 -0
  145. package/dist/types/services/core/websocket/types.d.ts +37 -0
  146. package/dist/types/services/index.d.ts +54 -0
  147. package/dist/types/services/task/AutoWrapup.d.ts +40 -0
  148. package/dist/types/services/task/Task.d.ts +157 -0
  149. package/dist/types/services/task/TaskFactory.d.ts +12 -0
  150. package/dist/types/services/task/TaskManager.d.ts +1 -0
  151. package/dist/types/services/task/TaskUtils.d.ts +138 -0
  152. package/dist/types/services/task/constants.d.ts +91 -0
  153. package/dist/types/services/task/contact.d.ts +69 -0
  154. package/dist/types/services/task/dialer.d.ts +73 -0
  155. package/dist/types/services/task/digital/Digital.d.ts +22 -0
  156. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1194 -0
  157. package/dist/types/services/task/state-machine/actions.d.ts +10 -0
  158. package/dist/types/services/task/state-machine/constants.d.ts +107 -0
  159. package/dist/types/services/task/state-machine/guards.d.ts +102 -0
  160. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  161. package/dist/types/services/task/state-machine/types.d.ts +269 -0
  162. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  163. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  164. package/dist/types/services/task/types.d.ts +1856 -0
  165. package/dist/types/services/task/voice/Voice.d.ts +184 -0
  166. package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
  167. package/dist/types/types.d.ts +778 -0
  168. package/dist/types/utils/PageCache.d.ts +173 -0
  169. package/dist/types/webex-config.d.ts +53 -0
  170. package/dist/types/webex.d.ts +8 -0
  171. package/dist/types.js +130 -1
  172. package/dist/types.js.map +1 -1
  173. package/dist/webex.js +14 -2
  174. package/dist/webex.js.map +1 -1
  175. package/package.json +16 -12
  176. package/src/cc.ts +477 -51
  177. package/src/config.ts +6 -0
  178. package/src/constants.ts +21 -1
  179. package/src/index.ts +24 -5
  180. package/src/metrics/ai-docs/AGENTS.md +350 -0
  181. package/src/metrics/ai-docs/ARCHITECTURE.md +338 -0
  182. package/src/metrics/ai-docs/metrics-spec.md +854 -0
  183. package/src/metrics/behavioral-events.ts +120 -0
  184. package/src/metrics/constants.ts +37 -3
  185. package/src/services/ApiAiAssistant.ts +412 -0
  186. package/src/services/UserPreference.ts +509 -0
  187. package/src/services/agent/ai-docs/AGENTS.md +240 -0
  188. package/src/services/agent/ai-docs/ARCHITECTURE.md +304 -0
  189. package/src/services/agent/ai-docs/agent-spec.md +504 -0
  190. package/src/services/agent/types.ts +1 -1
  191. package/src/services/ai-docs/AGENTS.md +386 -0
  192. package/src/services/ai-docs/services-spec.md +492 -0
  193. package/src/services/config/Util.ts +10 -2
  194. package/src/services/config/ai-docs/AGENTS.md +255 -0
  195. package/src/services/config/ai-docs/ARCHITECTURE.md +426 -0
  196. package/src/services/config/ai-docs/config-spec.md +669 -0
  197. package/src/services/config/constants.ts +37 -1
  198. package/src/services/config/index.ts +45 -1
  199. package/src/services/config/types.ts +241 -11
  200. package/src/services/constants.ts +29 -0
  201. package/src/services/core/Err.ts +3 -0
  202. package/src/services/core/Utils.ts +143 -30
  203. package/src/services/core/ai-docs/AGENTS.md +381 -0
  204. package/src/services/core/ai-docs/ARCHITECTURE.md +698 -0
  205. package/src/services/core/ai-docs/core-spec.md +783 -0
  206. package/src/services/core/aqm-reqs.ts +100 -22
  207. package/src/services/core/websocket/WebSocketManager.ts +23 -6
  208. package/src/services/core/websocket/connection-service.ts +5 -1
  209. package/src/services/core/websocket/types.ts +1 -1
  210. package/src/services/index.ts +4 -0
  211. package/src/services/task/Task.ts +837 -0
  212. package/src/services/task/TaskFactory.ts +55 -0
  213. package/src/services/task/TaskManager.ts +793 -521
  214. package/src/services/task/TaskUtils.ts +314 -24
  215. package/src/services/task/ai-docs/AGENTS.md +457 -0
  216. package/src/services/task/ai-docs/ARCHITECTURE.md +594 -0
  217. package/src/services/task/ai-docs/task-spec.md +1319 -0
  218. package/src/services/task/constants.ts +23 -0
  219. package/src/services/task/dialer.ts +136 -1
  220. package/src/services/task/digital/Digital.ts +95 -0
  221. package/src/services/task/state-machine/TaskStateMachine.ts +1166 -0
  222. package/src/services/task/state-machine/actions.ts +738 -0
  223. package/src/services/task/state-machine/ai-docs/AGENTS.md +458 -0
  224. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1137 -0
  225. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +2177 -0
  226. package/src/services/task/state-machine/constants.ts +172 -0
  227. package/src/services/task/state-machine/guards.ts +445 -0
  228. package/src/services/task/state-machine/index.ts +28 -0
  229. package/src/services/task/state-machine/types.ts +243 -0
  230. package/src/services/task/state-machine/uiControlsComputer.ts +961 -0
  231. package/src/services/task/taskDataNormalizer.ts +137 -0
  232. package/src/services/task/types.ts +734 -71
  233. package/src/services/task/voice/Voice.ts +1270 -0
  234. package/src/services/task/voice/WebRTC.ts +187 -0
  235. package/src/types.ts +205 -2
  236. package/src/utils/AGENTS.md +278 -0
  237. package/src/utils/ai-docs/utils-spec.md +381 -0
  238. package/src/webex.js +2 -0
  239. package/test/unit/spec/cc.ts +503 -43
  240. package/test/unit/spec/logger-proxy.ts +70 -0
  241. package/test/unit/spec/services/ApiAiAssistant.ts +273 -0
  242. package/test/unit/spec/services/UserPreference.ts +401 -0
  243. package/test/unit/spec/services/WebCallingService.ts +7 -1
  244. package/test/unit/spec/services/config/index.ts +85 -29
  245. package/test/unit/spec/services/core/Utils.ts +481 -2
  246. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +137 -41
  247. package/test/unit/spec/services/core/websocket/connection-service.ts +3 -1
  248. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  249. package/test/unit/spec/services/task/Task.ts +477 -0
  250. package/test/unit/spec/services/task/TaskFactory.ts +62 -0
  251. package/test/unit/spec/services/task/TaskManager.ts +1001 -1003
  252. package/test/unit/spec/services/task/TaskUtils.ts +235 -0
  253. package/test/unit/spec/services/task/dialer.ts +372 -96
  254. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  255. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +2651 -0
  256. package/test/unit/spec/services/task/state-machine/guards.ts +637 -0
  257. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  258. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +2663 -0
  259. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  260. package/test/unit/spec/services/task/voice/Voice.ts +649 -0
  261. package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
  262. package/umd/contact-center.min.js +2 -2
  263. package/umd/contact-center.min.js.map +1 -1
  264. package/dist/services/task/index.js +0 -1525
  265. package/dist/services/task/index.js.map +0 -1
  266. package/src/services/task/index.ts +0 -1801
  267. package/test/unit/spec/services/task/index.ts +0 -2184
@@ -0,0 +1,1270 @@
1
+ import {CC_FILE, METHODS} from '../../../constants';
2
+ import {
3
+ buildConsultConferenceParamData,
4
+ calculateDestAgentId,
5
+ calculateDestType,
6
+ getErrorDetails,
7
+ } from '../../core/Utils';
8
+ import routingContact from '../contact';
9
+ import {
10
+ ConsultPayload,
11
+ ConsultEndPayload,
12
+ ResumeRecordingPayload,
13
+ TaskData,
14
+ TaskResponse,
15
+ IVoice,
16
+ VoiceUIControlOptions,
17
+ TransferPayLoad,
18
+ ConsultTransferPayLoad,
19
+ consultConferencePayloadData,
20
+ CONSULT_TRANSFER_DESTINATION_TYPE,
21
+ TASK_EVENTS,
22
+ VOICE_VARIANT,
23
+ } from '../types';
24
+ import Task from '../Task';
25
+ import LoggerProxy from '../../../logger-proxy';
26
+ import MetricsManager from '../../../metrics/MetricsManager';
27
+ import {METRIC_EVENT_NAMES} from '../../../metrics/constants';
28
+ import {TaskState, TaskEvent, TaskActionArgs} from '../state-machine';
29
+ import {WrapupData} from '../../config/types';
30
+ import {getConsultMediaResourceId, getIsConferenceInProgress} from '../TaskUtils';
31
+
32
+ export default class Voice extends Task implements IVoice {
33
+ constructor(
34
+ contact: ReturnType<typeof routingContact>,
35
+ data: TaskData,
36
+ callOptions?: VoiceUIControlOptions,
37
+ wrapupData?: WrapupData,
38
+ agentId?: string
39
+ ) {
40
+ const resolvedOptions = {
41
+ isEndTaskEnabled: callOptions?.isEndTaskEnabled ?? true,
42
+ isEndConsultEnabled: callOptions?.isEndConsultEnabled ?? true,
43
+ voiceVariant: callOptions?.voiceVariant ?? VOICE_VARIANT.PSTN,
44
+ isRecordingEnabled: callOptions?.isRecordingEnabled ?? true,
45
+ };
46
+
47
+ super(
48
+ contact,
49
+ data,
50
+ {
51
+ ...resolvedOptions,
52
+ },
53
+ wrapupData,
54
+ agentId
55
+ );
56
+ }
57
+
58
+ private getStateMachineSnapshot() {
59
+ return this.stateMachineService?.getSnapshot?.();
60
+ }
61
+
62
+ /**
63
+ * This method is used to accept the task.
64
+ * It is expected to be overridden by child classes.
65
+ * @returns Promise<TaskResponse>
66
+ * @throws Error
67
+ */
68
+ public async accept(): Promise<TaskResponse> {
69
+ super.unsupportedMethodError(METHODS.ACCEPT);
70
+ }
71
+
72
+ /**
73
+ * This method is used to decline the task.
74
+ * It is expected to be overridden by child classes.
75
+ * @returns Promise<TaskResponse>
76
+ * @throws Error
77
+ */
78
+ public async decline(): Promise<TaskResponse> {
79
+ super.unsupportedMethodError(METHODS.REJECT);
80
+ }
81
+
82
+ /**
83
+ * This is used to hold the task.
84
+ * @returns Promise<TaskResponse>
85
+ * @throws Error
86
+ * @example
87
+ * ```typescript
88
+ * task.hold().then(()=>{}).catch(()=>{})
89
+ * ```
90
+ * */
91
+ public async hold(): Promise<TaskResponse> {
92
+ return this.holdResume();
93
+ }
94
+
95
+ /**
96
+ * This is used to resume the task.
97
+ * @returns Promise<TaskResponse>
98
+ * @throws Error
99
+ * @example
100
+ * ```typescript
101
+ * task.resume().then(()=>{}).catch(()=>{})
102
+ * ```
103
+ * */
104
+ public async resume(): Promise<TaskResponse> {
105
+ return this.holdResume();
106
+ }
107
+
108
+ /**
109
+ * This is used to hold or resume the task.
110
+ * @param isHeld: boolean - true to hold the task, false to resume it
111
+ * @returns Promise<TaskResponse>
112
+ * @throws Error
113
+ * @example
114
+ * ```typescript
115
+ * task.holdResume(isHeld: true).then(()=>{}).catch(()=>{})
116
+ * ```
117
+ * */
118
+ public async holdResume(): Promise<TaskResponse> {
119
+ /*
120
+ Determine if the task is being held or resumed based on the media resource state
121
+ If the media resource is not found, default to resuming the task
122
+ */
123
+ const snapshot = this.getStateMachineSnapshot();
124
+ const snapshotState = snapshot?.value as TaskState | undefined;
125
+ const mainInteractionId = this.data.interaction?.mainInteractionId || this.data.interactionId;
126
+ const mainMediaResource =
127
+ this.data.interaction?.media?.[mainInteractionId]?.mediaResourceId ||
128
+ this.data.mediaResourceId;
129
+ const mediaHoldState =
130
+ this.data.interaction?.media?.[mainInteractionId]?.isHold ??
131
+ this.data.interaction.media?.[mainMediaResource]?.isHold;
132
+ let shouldHold = !(mediaHoldState ?? false);
133
+ if (snapshotState === TaskState.HELD || snapshotState === TaskState.RESUME_INITIATING) {
134
+ shouldHold = false;
135
+ } else if (snapshotState === TaskState.CONNECTED && mediaHoldState !== true) {
136
+ // CONNECTED with isHold:true can happen after consult RONA/decline when event ordering
137
+ // leaves the machine in CONNECTED while main media is still on hold — resume on first click.
138
+ shouldHold = true;
139
+ }
140
+
141
+ // Validate operation is allowed in current state
142
+ const state = snapshot;
143
+ if (state) {
144
+ const currentState = state.value as TaskState;
145
+ if (shouldHold) {
146
+ if (!state.matches(TaskState.CONNECTED)) {
147
+ const error = new Error(`Cannot hold call in current state: ${currentState}`);
148
+ LoggerProxy.error('Hold operation not allowed', {
149
+ module: CC_FILE,
150
+ method: METHODS.HOLD_RESUME,
151
+ interactionId: this.data.interactionId,
152
+ });
153
+ throw error;
154
+ }
155
+ } else if (
156
+ !state.matches(TaskState.HELD) &&
157
+ !(state.matches(TaskState.CONFERENCING) && mediaHoldState === true) &&
158
+ !(state.matches(TaskState.CONNECTED) && mediaHoldState === true)
159
+ ) {
160
+ const error = new Error(`Cannot resume call in current state: ${currentState}`);
161
+ LoggerProxy.error('Resume operation not allowed', {
162
+ module: CC_FILE,
163
+ method: METHODS.HOLD_RESUME,
164
+ interactionId: this.data.interactionId,
165
+ });
166
+ throw error;
167
+ }
168
+ }
169
+
170
+ // Send initiating event to transition to intermediate state
171
+ if (this.stateMachineService) {
172
+ const initiatingEvent = shouldHold ? TaskEvent.HOLD_INITIATED : TaskEvent.UNHOLD_INITIATED;
173
+ this.stateMachineService.send({
174
+ type: initiatingEvent,
175
+ mediaResourceId: mainMediaResource,
176
+ });
177
+ }
178
+
179
+ LoggerProxy.info(`${shouldHold ? 'Holding' : 'Resuming'} task`, {
180
+ module: CC_FILE,
181
+ method: METHODS.HOLD_RESUME,
182
+ interactionId: this.data.interactionId,
183
+ });
184
+ const [successEvt, failedEvt] = shouldHold
185
+ ? [METRIC_EVENT_NAMES.TASK_HOLD_SUCCESS, METRIC_EVENT_NAMES.TASK_HOLD_FAILED]
186
+ : [METRIC_EVENT_NAMES.TASK_RESUME_SUCCESS, METRIC_EVENT_NAMES.TASK_RESUME_FAILED];
187
+
188
+ this.metricsManager.timeEvent([successEvt, failedEvt]);
189
+
190
+ try {
191
+ let response: TaskResponse;
192
+ if (shouldHold) {
193
+ response = await this.contact.hold({
194
+ interactionId: this.data.interactionId,
195
+ data: {mediaResourceId: mainMediaResource},
196
+ });
197
+
198
+ // Send success event to complete the transition
199
+ if (this.stateMachineService) {
200
+ this.stateMachineService.send({
201
+ type: TaskEvent.HOLD_SUCCESS,
202
+ mediaResourceId: mainMediaResource,
203
+ });
204
+ }
205
+
206
+ this.metricsManager.trackEvent(
207
+ successEvt,
208
+ {
209
+ taskId: this.data.interactionId,
210
+ mediaResourceId: mainMediaResource,
211
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
212
+ },
213
+ ['operational', 'behavioral']
214
+ );
215
+ LoggerProxy.log(`Task placed on hold successfully`, {
216
+ module: CC_FILE,
217
+ method: METHODS.HOLD_RESUME,
218
+ trackingId: response.trackingId,
219
+ interactionId: this.data.interactionId,
220
+ });
221
+ } else {
222
+ response = await this.contact.unHold({
223
+ interactionId: this.data.interactionId,
224
+ data: {mediaResourceId: mainMediaResource},
225
+ });
226
+
227
+ // Send success event to complete the transition
228
+ if (this.stateMachineService) {
229
+ this.stateMachineService.send({
230
+ type: TaskEvent.UNHOLD_SUCCESS,
231
+ mediaResourceId: mainMediaResource,
232
+ });
233
+ }
234
+
235
+ this.metricsManager.trackEvent(
236
+ successEvt,
237
+ {
238
+ taskId: this.data.interactionId,
239
+ mainInteractionId,
240
+ mediaResourceId: mainMediaResource,
241
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
242
+ },
243
+ ['operational', 'behavioral']
244
+ );
245
+ LoggerProxy.log(`Task resumed successfully`, {
246
+ module: CC_FILE,
247
+ method: METHODS.HOLD_RESUME,
248
+ trackingId: response.trackingId,
249
+ interactionId: this.data.interactionId,
250
+ });
251
+ }
252
+
253
+ return response;
254
+ } catch (error) {
255
+ const failureEvent = shouldHold ? TaskEvent.HOLD_FAILED : TaskEvent.UNHOLD_FAILED;
256
+ this.stateMachineService.send({
257
+ type: failureEvent,
258
+ reason: error.toString(),
259
+ mediaResourceId: this.data.mediaResourceId,
260
+ });
261
+
262
+ const {error: detailedError} = getErrorDetails(error, 'holdResume', CC_FILE);
263
+ this.metricsManager.trackEvent(
264
+ failedEvt,
265
+ shouldHold
266
+ ? {
267
+ taskId: this.data.interactionId,
268
+ mediaResourceId: this.data.mediaResourceId,
269
+ error: error.toString(),
270
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
271
+ }
272
+ : {
273
+ taskId: this.data.interactionId,
274
+ error: error.toString(),
275
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
276
+ },
277
+ ['operational', 'behavioral']
278
+ );
279
+ throw detailedError;
280
+ }
281
+ }
282
+
283
+ /**
284
+ * This is used to pause the call recording
285
+ * @returns Promise<TaskResponse>
286
+ * @throws Error
287
+ * @example
288
+ * ```typescript
289
+ * task.pauseRecording().then(()=>{}).catch(()=>{});
290
+ * ```
291
+ */
292
+ public async pauseRecording(): Promise<TaskResponse> {
293
+ // Validate recording is active
294
+ const state = this.getStateMachineSnapshot();
295
+ if (state) {
296
+ const {recordingControlsAvailable, recordingInProgress} = state.context as {
297
+ recordingControlsAvailable?: boolean;
298
+ recordingInProgress?: boolean;
299
+ };
300
+ const recordingActive = Boolean(recordingControlsAvailable && recordingInProgress);
301
+ if (!recordingActive) {
302
+ const error = new Error('Recording is not active or already paused');
303
+ LoggerProxy.error('Pause recording operation not allowed', {
304
+ module: CC_FILE,
305
+ method: 'pauseRecording',
306
+ interactionId: this.data.interactionId,
307
+ });
308
+ throw error;
309
+ }
310
+ }
311
+
312
+ try {
313
+ LoggerProxy.info(`Pausing recording`, {
314
+ module: CC_FILE,
315
+ method: 'pauseRecording',
316
+ interactionId: this.data.interactionId,
317
+ });
318
+ this.metricsManager.timeEvent([
319
+ METRIC_EVENT_NAMES.TASK_PAUSE_RECORDING_SUCCESS,
320
+ METRIC_EVENT_NAMES.TASK_PAUSE_RECORDING_FAILED,
321
+ ]);
322
+ const result = await this.contact.pauseRecording({interactionId: this.data.interactionId});
323
+ this.metricsManager.trackEvent(
324
+ METRIC_EVENT_NAMES.TASK_PAUSE_RECORDING_SUCCESS,
325
+ {
326
+ taskId: this.data.interactionId,
327
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(result),
328
+ },
329
+ ['operational', 'behavioral', 'business']
330
+ );
331
+ LoggerProxy.log(`Recording paused successfully`, {
332
+ module: CC_FILE,
333
+ method: 'pauseRecording',
334
+ trackingId: result.trackingId,
335
+ interactionId: this.data.interactionId,
336
+ });
337
+
338
+ return result;
339
+ } catch (error) {
340
+ const {error: detailedError} = getErrorDetails(error, 'pauseRecording', CC_FILE);
341
+ this.metricsManager.trackEvent(
342
+ METRIC_EVENT_NAMES.TASK_PAUSE_RECORDING_FAILED,
343
+ {
344
+ taskId: this.data.interactionId,
345
+ error: error.toString(),
346
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
347
+ },
348
+ ['operational', 'behavioral', 'business']
349
+ );
350
+ throw detailedError;
351
+ }
352
+ }
353
+
354
+ /**
355
+ * This is used to pause the call recording
356
+ * @param resumeRecordingPayload
357
+ * @returns Promise<TaskResponse>
358
+ * @throws Error
359
+ * @example
360
+ * ```typescript
361
+ * task.resumeRecording(resumeRecordingPayload).then(()=>{}).catch(()=>{});
362
+ * ```
363
+ */
364
+ public async resumeRecording(
365
+ resumeRecordingPayload?: ResumeRecordingPayload
366
+ ): Promise<TaskResponse> {
367
+ // Validate recording is paused
368
+ const state = this.getStateMachineSnapshot();
369
+ if (state) {
370
+ const {recordingControlsAvailable, recordingInProgress} = state.context as {
371
+ recordingControlsAvailable?: boolean;
372
+ recordingInProgress?: boolean;
373
+ };
374
+ const recordingPaused = Boolean(recordingControlsAvailable && !recordingInProgress);
375
+ if (!recordingPaused) {
376
+ const error = new Error('Recording is not paused');
377
+ LoggerProxy.error('Resume recording operation not allowed', {
378
+ module: CC_FILE,
379
+ method: 'resumeRecording',
380
+ interactionId: this.data.interactionId,
381
+ });
382
+ throw error;
383
+ }
384
+ }
385
+
386
+ try {
387
+ LoggerProxy.info(`Resuming recording`, {
388
+ module: CC_FILE,
389
+ method: 'resumeRecording',
390
+ interactionId: this.data.interactionId,
391
+ });
392
+ this.metricsManager.timeEvent([
393
+ METRIC_EVENT_NAMES.TASK_RESUME_RECORDING_SUCCESS,
394
+ METRIC_EVENT_NAMES.TASK_RESUME_RECORDING_FAILED,
395
+ ]);
396
+ resumeRecordingPayload ??= {autoResumed: false};
397
+
398
+ const result = await this.contact.resumeRecording({
399
+ interactionId: this.data.interactionId,
400
+ data: resumeRecordingPayload,
401
+ });
402
+ this.metricsManager.trackEvent(
403
+ METRIC_EVENT_NAMES.TASK_RESUME_RECORDING_SUCCESS,
404
+ {
405
+ taskId: this.data.interactionId,
406
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(result),
407
+ },
408
+ ['operational', 'behavioral', 'business']
409
+ );
410
+ LoggerProxy.log(`Recording resumed successfully`, {
411
+ module: CC_FILE,
412
+ method: 'resumeRecording',
413
+ trackingId: result.trackingId,
414
+ interactionId: this.data.interactionId,
415
+ });
416
+
417
+ return result;
418
+ } catch (error) {
419
+ const {error: detailedError} = getErrorDetails(error, 'resumeRecording', CC_FILE);
420
+ this.metricsManager.trackEvent(
421
+ METRIC_EVENT_NAMES.TASK_RESUME_RECORDING_FAILED,
422
+ {
423
+ taskId: this.data.interactionId,
424
+ error: error.toString(),
425
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
426
+ },
427
+ ['operational', 'behavioral', 'business']
428
+ );
429
+ throw detailedError;
430
+ }
431
+ }
432
+
433
+ /**
434
+ * This is used to consult the task
435
+ * @param consultPayload
436
+ * @returns Promise<TaskResponse>
437
+ * @throws Error
438
+ * @example
439
+ * ```typescript
440
+ * const consultPayload = {
441
+ * destination: 'myBuddyAgentId',
442
+ * destinationType: DESTINATION_TYPE.AGENT,
443
+ * }
444
+ * task.consult(consultPayload).then(()=>{}).catch(()=>{});
445
+ * ```
446
+ * */
447
+ public async consult(consultPayload?: ConsultPayload): Promise<TaskResponse> {
448
+ // Validate consult is allowed
449
+ const state = this.getStateMachineSnapshot();
450
+ const canConsult =
451
+ state &&
452
+ (state.matches(TaskState.CONNECTED) ||
453
+ state.matches(TaskState.HELD) ||
454
+ state.matches(TaskState.CONFERENCING));
455
+
456
+ if (!canConsult) {
457
+ const currentState = state?.value as TaskState;
458
+ const error = new Error(`Cannot initiate consult in ${currentState} state`);
459
+ LoggerProxy.error('Consult operation not allowed', {
460
+ module: CC_FILE,
461
+ method: 'consult',
462
+ interactionId: this.data.interactionId,
463
+ });
464
+ throw error;
465
+ }
466
+
467
+ // Send initiating event to transition to CONSULT_INITIATING state
468
+ if (this.stateMachineService) {
469
+ this.stateMachineService.send({
470
+ type: TaskEvent.CONSULT,
471
+ destination: consultPayload.to,
472
+ destinationType: consultPayload.destinationType,
473
+ });
474
+ }
475
+
476
+ const requestInteractionId =
477
+ this.data.interaction?.mainInteractionId || this.data.interactionId;
478
+
479
+ try {
480
+ LoggerProxy.info(`Starting consult`, {
481
+ module: CC_FILE,
482
+ method: 'consult',
483
+ interactionId: requestInteractionId,
484
+ });
485
+ this.metricsManager.timeEvent([
486
+ METRIC_EVENT_NAMES.TASK_CONSULT_START_SUCCESS,
487
+ METRIC_EVENT_NAMES.TASK_CONSULT_START_FAILED,
488
+ ]);
489
+ const result = await this.contact.consult({
490
+ interactionId: requestInteractionId,
491
+ data: consultPayload,
492
+ });
493
+
494
+ // Send success event to transition to CONSULTING state
495
+ if (this.stateMachineService) {
496
+ this.stateMachineService.send({
497
+ type: TaskEvent.CONSULT_SUCCESS,
498
+ taskData: result.data,
499
+ });
500
+ }
501
+
502
+ this.metricsManager.trackEvent(
503
+ METRIC_EVENT_NAMES.TASK_CONSULT_START_SUCCESS,
504
+ {
505
+ taskId: this.data.interactionId,
506
+ requestInteractionId,
507
+ destination: consultPayload.to,
508
+ destinationType: consultPayload.destinationType,
509
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(result),
510
+ },
511
+ ['operational', 'behavioral', 'business']
512
+ );
513
+ LoggerProxy.log(`Consult successfully initiated to ${consultPayload.to}`, {
514
+ module: CC_FILE,
515
+ method: 'consult',
516
+ trackingId: result.trackingId,
517
+ interactionId: requestInteractionId,
518
+ });
519
+
520
+ return result;
521
+ } catch (error) {
522
+ this.stateMachineService.send({
523
+ type: TaskEvent.CONSULT_FAILED,
524
+ reason: error.toString(),
525
+ });
526
+
527
+ const {error: detailedError} = getErrorDetails(error, 'consult', CC_FILE);
528
+ this.metricsManager.trackEvent(
529
+ METRIC_EVENT_NAMES.TASK_CONSULT_START_FAILED,
530
+ {
531
+ taskId: this.data.interactionId,
532
+ requestInteractionId,
533
+ destination: consultPayload.to,
534
+ destinationType: consultPayload.destinationType,
535
+ error: error.toString(),
536
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
537
+ },
538
+ ['operational', 'behavioral', 'business']
539
+ );
540
+ throw detailedError;
541
+ }
542
+ }
543
+
544
+ /**
545
+ * This is used to end the consult session on the task.
546
+ * @param consultEndPayload - Payload indicating consult end flags and identifiers
547
+ * @returns Promise<TaskResponse>
548
+ * @throws Error
549
+ * @example
550
+ * ```typescript
551
+ * task.endConsult({
552
+ * isConsult: true,
553
+ * queueId: 'myQueueId',
554
+ * taskId: 'taskId',
555
+ * });
556
+ * ```
557
+ */
558
+ public async endConsult(consultEndPayload?: ConsultEndPayload): Promise<TaskResponse> {
559
+ const requestInteractionId =
560
+ this.data.interaction?.mainInteractionId || this.data.interactionId;
561
+
562
+ try {
563
+ LoggerProxy.info(`Ending consult`, {
564
+ module: CC_FILE,
565
+ method: 'endConsult',
566
+ interactionId: requestInteractionId,
567
+ });
568
+ this.metricsManager.timeEvent([
569
+ METRIC_EVENT_NAMES.TASK_CONSULT_END_SUCCESS,
570
+ METRIC_EVENT_NAMES.TASK_CONSULT_END_FAILED,
571
+ ]);
572
+ const result = await this.contact.consultEnd({
573
+ interactionId: requestInteractionId,
574
+ data: consultEndPayload,
575
+ });
576
+ this.metricsManager.trackEvent(
577
+ METRIC_EVENT_NAMES.TASK_CONSULT_END_SUCCESS,
578
+ {
579
+ taskId: this.data.interactionId,
580
+ requestInteractionId,
581
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(result),
582
+ },
583
+ ['operational', 'behavioral', 'business']
584
+ );
585
+ LoggerProxy.log(`Consult ended successfully`, {
586
+ module: CC_FILE,
587
+ method: 'endConsult',
588
+ trackingId: result.trackingId,
589
+ interactionId: this.data.interactionId,
590
+ });
591
+
592
+ return result;
593
+ } catch (error) {
594
+ const {error: detailedError} = getErrorDetails(error, 'endConsult', CC_FILE);
595
+ this.metricsManager.trackEvent(
596
+ METRIC_EVENT_NAMES.TASK_CONSULT_END_FAILED,
597
+ {
598
+ taskId: this.data.interactionId,
599
+ requestInteractionId,
600
+ error: error.toString(),
601
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
602
+ },
603
+ ['operational', 'behavioral', 'business']
604
+ );
605
+ throw detailedError;
606
+ }
607
+ }
608
+
609
+ /**
610
+ * This is used to transfer the task.
611
+ * @param payload - Transfer payload
612
+ * @returns Promise<TaskResponse>
613
+ * @throws Error
614
+ * @example
615
+ * ```typescript
616
+ * task.transfer({
617
+ * to: 'destinationId',
618
+ * destinationType: DESTINATION_TYPE.AGENT,
619
+ * consult: true, // Optional, if true will perform a consult transfer else blind transfer
620
+ * });
621
+ * ```
622
+ */
623
+ public async transfer(payload: TransferPayLoad): Promise<TaskResponse> {
624
+ try {
625
+ LoggerProxy.info(`Transferring task to ${payload.to}`, {
626
+ module: CC_FILE,
627
+ method: METHODS.TRANSFER_CALL,
628
+ interactionId: this.data.interactionId,
629
+ });
630
+ this.metricsManager.timeEvent([
631
+ METRIC_EVENT_NAMES.TASK_TRANSFER_SUCCESS,
632
+ METRIC_EVENT_NAMES.TASK_TRANSFER_FAILED,
633
+ ]);
634
+
635
+ // consult transfer path
636
+ if (this.data.interaction.state === 'consulting') {
637
+ const normalizedDestinationType =
638
+ payload.destinationType === 'Agent' || payload.destinationType === 'Queue'
639
+ ? (payload.destinationType.toLowerCase() as ConsultTransferPayLoad['destinationType'])
640
+ : payload.destinationType;
641
+ let consultPayload: ConsultTransferPayLoad = {
642
+ to: payload.to,
643
+ destinationType: normalizedDestinationType,
644
+ };
645
+
646
+ if (normalizedDestinationType === CONSULT_TRANSFER_DESTINATION_TYPE.QUEUE) {
647
+ const consultContext = this.getStateMachineSnapshot()?.context;
648
+ const destAgent = consultContext?.consultDestinationAgentId || this.data.destAgentId;
649
+ if (!destAgent) {
650
+ throw new Error('No agent has accepted this queue consult yet');
651
+ }
652
+ consultPayload = {
653
+ to: destAgent,
654
+ destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.AGENT,
655
+ };
656
+ }
657
+
658
+ const result = await this.contact.consultTransfer({
659
+ interactionId: this.data.interactionId,
660
+ data: consultPayload,
661
+ });
662
+ this.metricsManager.trackEvent(
663
+ METRIC_EVENT_NAMES.TASK_TRANSFER_SUCCESS,
664
+ {
665
+ taskId: this.data.interactionId,
666
+ destination: consultPayload.to,
667
+ destinationType: consultPayload.destinationType,
668
+ isConsultTransfer: true,
669
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(result),
670
+ },
671
+ ['operational', 'behavioral', 'business']
672
+ );
673
+ LoggerProxy.log(`Consult transfer completed successfully to ${consultPayload.to}`, {
674
+ module: CC_FILE,
675
+ method: METHODS.TRANSFER_CALL,
676
+ trackingId: result.trackingId,
677
+ interactionId: this.data.interactionId,
678
+ });
679
+
680
+ return result;
681
+ }
682
+
683
+ // standard blind transfer
684
+ return await super.transfer({
685
+ to: payload.to,
686
+ destinationType: payload.destinationType,
687
+ });
688
+ } catch (err) {
689
+ const {error: detailedError} = getErrorDetails(err, METHODS.TRANSFER_CALL, CC_FILE);
690
+ this.metricsManager.trackEvent(
691
+ METRIC_EVENT_NAMES.TASK_TRANSFER_FAILED,
692
+ {
693
+ taskId: this.data.interactionId,
694
+ destination: payload.to,
695
+ destinationType: payload.destinationType,
696
+ isConsultTransfer: this.data.interaction.state === 'consulting',
697
+ error: err.toString(),
698
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(err.details || {}),
699
+ },
700
+ ['operational', 'behavioral', 'business']
701
+ );
702
+ throw detailedError;
703
+ }
704
+ }
705
+
706
+ /**
707
+ * Start a consult conference, merging main and consult calls.
708
+ */
709
+ public async consultConference(): Promise<TaskResponse> {
710
+ const derivedDestAgentId =
711
+ this.data.interaction && this.data.agentId
712
+ ? calculateDestAgentId(this.data.interaction, this.data.agentId)
713
+ : '';
714
+ const derivedDestType =
715
+ this.data.interaction && this.data.agentId
716
+ ? calculateDestType(this.data.interaction, this.data.agentId)
717
+ : '';
718
+
719
+ // derivedDestType is most reliable as it inspects live interaction participants
720
+ const resolvedDestinationType =
721
+ derivedDestType ||
722
+ this.getStateMachineSnapshot()?.context?.consultDestinationType ||
723
+ this.data.destinationType ||
724
+ 'agent';
725
+
726
+ const consultationData: consultConferencePayloadData = {
727
+ agentId: this.data.agentId,
728
+ destinationType: resolvedDestinationType,
729
+ // derivedDestAgentId is most reliable as it resolves epId for EP_DN
730
+ // and agent ID for regular agents from live interaction data
731
+ destAgentId:
732
+ derivedDestAgentId ||
733
+ this.getStateMachineSnapshot()?.context?.consultDestinationAgentId ||
734
+ this.data.destAgentId,
735
+ };
736
+
737
+ // Send state machine event to transition to CONF_INITIATING
738
+ if (this.stateMachineService) {
739
+ this.stateMachineService.send({
740
+ type: TaskEvent.MERGE_TO_CONFERENCE,
741
+ });
742
+ }
743
+
744
+ try {
745
+ if (!consultationData.destAgentId) {
746
+ throw new Error('Unable to determine consult destination for conference');
747
+ }
748
+
749
+ LoggerProxy.info(`Initiating consult conference to ${consultationData.destAgentId}`, {
750
+ module: CC_FILE,
751
+ method: METHODS.CONSULT_CONFERENCE,
752
+ interactionId: this.data.interactionId,
753
+ });
754
+
755
+ const paramsDataForConferenceV2 = buildConsultConferenceParamData(
756
+ consultationData,
757
+ this.data.interactionId
758
+ );
759
+
760
+ const response = await this.contact.consultConference({
761
+ interactionId: paramsDataForConferenceV2.interactionId,
762
+ data: paramsDataForConferenceV2.data,
763
+ });
764
+
765
+ // Send success event to transition to CONFERENCING
766
+ if (this.stateMachineService) {
767
+ this.stateMachineService.send({
768
+ type: TaskEvent.CONFERENCE_START,
769
+ });
770
+ }
771
+
772
+ this.metricsManager.trackEvent(
773
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_START_SUCCESS,
774
+ {
775
+ taskId: this.data.interactionId,
776
+ destination: paramsDataForConferenceV2.data.to,
777
+ destinationType: paramsDataForConferenceV2.data.destinationType,
778
+ agentId: paramsDataForConferenceV2.data.agentId,
779
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
780
+ },
781
+ ['operational', 'behavioral', 'business']
782
+ );
783
+
784
+ LoggerProxy.log(`Consult conference started successfully`, {
785
+ module: CC_FILE,
786
+ method: METHODS.CONSULT_CONFERENCE,
787
+ interactionId: this.data.interactionId,
788
+ });
789
+
790
+ return response;
791
+ } catch (error) {
792
+ // Send failure event to revert state
793
+ if (this.stateMachineService) {
794
+ this.stateMachineService.send({
795
+ type: TaskEvent.CONFERENCE_FAILED,
796
+ reason: error.toString(),
797
+ });
798
+ }
799
+
800
+ const {error: detailedError} = getErrorDetails(error, METHODS.CONSULT_CONFERENCE, CC_FILE);
801
+
802
+ const failedParamsData = buildConsultConferenceParamData(
803
+ consultationData,
804
+ this.data.interactionId
805
+ );
806
+
807
+ this.metricsManager.trackEvent(
808
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_START_FAILED,
809
+ {
810
+ taskId: this.data.interactionId,
811
+ destination: failedParamsData.data.to,
812
+ destinationType: failedParamsData.data.destinationType,
813
+ agentId: failedParamsData.data.agentId,
814
+ error: error.toString(),
815
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
816
+ },
817
+ ['operational', 'behavioral', 'business']
818
+ );
819
+
820
+ LoggerProxy.error(`Failed to start consult conference`, {
821
+ module: CC_FILE,
822
+ method: METHODS.CONSULT_CONFERENCE,
823
+ interactionId: this.data.interactionId,
824
+ });
825
+
826
+ throw detailedError;
827
+ }
828
+ }
829
+
830
+ /**
831
+ * Exit from a conference call.
832
+ * Per conference-spec.md:
833
+ * - Primary agent exits to wrapup
834
+ * - Non-primary agent exits to available/connected
835
+ * - Other participants continue the call
836
+ *
837
+ * @returns Promise<TaskResponse>
838
+ * @throws Error if not in conference or exit fails
839
+ * @example
840
+ * ```typescript
841
+ * task.exitConference().then(() => {}).catch(() => {});
842
+ * ```
843
+ */
844
+ public async exitConference(): Promise<TaskResponse> {
845
+ // Validate we're in conference state OR conference is in progress per task data
846
+ // This handles cases where:
847
+ // 1. State machine is in CONFERENCING state
848
+ // 2. State machine is in CONNECTED but conference is active (e.g., ownership transferred)
849
+ const state = this.getStateMachineSnapshot();
850
+ const isConferencingState = state?.matches(TaskState.CONFERENCING);
851
+
852
+ const isConferenceInProgressFromData = this.data ? getIsConferenceInProgress(this.data) : false;
853
+
854
+ if (!state || (!isConferencingState && !isConferenceInProgressFromData)) {
855
+ const currentState = state?.value as TaskState;
856
+ const error = new Error(`Cannot exit conference in ${currentState} state`);
857
+ LoggerProxy.error('Exit conference operation not allowed', {
858
+ module: CC_FILE,
859
+ method: 'exitConference',
860
+ interactionId: this.data.interactionId,
861
+ });
862
+ throw error;
863
+ }
864
+
865
+ // Send state machine event
866
+ if (this.stateMachineService) {
867
+ this.stateMachineService.send({
868
+ type: TaskEvent.EXIT_CONFERENCE,
869
+ agentId: this.data.agentId,
870
+ });
871
+ }
872
+
873
+ const requestInteractionId =
874
+ this.data.interaction?.mainInteractionId || this.data.interactionId;
875
+
876
+ try {
877
+ LoggerProxy.info(`Exiting conference`, {
878
+ module: CC_FILE,
879
+ method: 'exitConference',
880
+ interactionId: requestInteractionId,
881
+ });
882
+
883
+ this.metricsManager.timeEvent([
884
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_EXIT_SUCCESS,
885
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_EXIT_FAILED,
886
+ ]);
887
+
888
+ const response = await this.contact.exitConference({
889
+ interactionId: requestInteractionId,
890
+ });
891
+
892
+ // Send success event to transition state
893
+ if (this.stateMachineService) {
894
+ this.stateMachineService.send({
895
+ type: TaskEvent.EXIT_CONFERENCE_SUCCESS,
896
+ taskData: response.data,
897
+ });
898
+ }
899
+
900
+ this.metricsManager.trackEvent(
901
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_EXIT_SUCCESS,
902
+ {
903
+ taskId: this.data.interactionId,
904
+ requestInteractionId,
905
+ agentId: this.data.agentId,
906
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
907
+ },
908
+ ['operational', 'behavioral', 'business']
909
+ );
910
+
911
+ LoggerProxy.log(`Successfully exited conference`, {
912
+ module: CC_FILE,
913
+ method: 'exitConference',
914
+ trackingId: response.trackingId,
915
+ interactionId: requestInteractionId,
916
+ });
917
+
918
+ return response;
919
+ } catch (error) {
920
+ // Send failure event
921
+ if (this.stateMachineService) {
922
+ this.stateMachineService.send({
923
+ type: TaskEvent.EXIT_CONFERENCE_FAILED,
924
+ reason: error.toString(),
925
+ });
926
+ }
927
+
928
+ const {error: detailedError} = getErrorDetails(error, 'exitConference', CC_FILE);
929
+ this.metricsManager.trackEvent(
930
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_EXIT_FAILED,
931
+ {
932
+ taskId: this.data.interactionId,
933
+ requestInteractionId,
934
+ agentId: this.data.agentId,
935
+ error: error.toString(),
936
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
937
+ },
938
+ ['operational', 'behavioral', 'business']
939
+ );
940
+
941
+ LoggerProxy.error(`Failed to exit conference`, {
942
+ module: CC_FILE,
943
+ method: 'exitConference',
944
+ interactionId: requestInteractionId,
945
+ });
946
+
947
+ throw detailedError;
948
+ }
949
+ }
950
+
951
+ /**
952
+ * Transfer the conference to another participant.
953
+ * Per conference-spec.md: Only primary agent can transfer conference.
954
+ * After transfer, the transferring agent exits to wrapup.
955
+ *
956
+ * @returns Promise<TaskResponse>
957
+ * @throws Error if not in conference or transfer fails
958
+ * @example
959
+ * ```typescript
960
+ * task.transferConference().then(() => {}).catch(() => {});
961
+ * ```
962
+ */
963
+ public async transferConference(): Promise<TaskResponse> {
964
+ // Validate we're in conference or consulting state
965
+ // CONSULTING is allowed because agent can transfer conference while consulting
966
+ // (transfers ownership to the consulted agent)
967
+ const state = this.getStateMachineSnapshot();
968
+ const isValidState =
969
+ state && (state.matches(TaskState.CONFERENCING) || state.matches(TaskState.CONSULTING));
970
+ if (!isValidState) {
971
+ const currentState = state?.value as TaskState;
972
+ const error = new Error(`Cannot transfer conference in ${currentState} state`);
973
+ LoggerProxy.error('Transfer conference operation not allowed', {
974
+ module: CC_FILE,
975
+ method: 'transferConference',
976
+ interactionId: this.data.interactionId,
977
+ });
978
+ throw error;
979
+ }
980
+
981
+ // Send state machine event
982
+ if (this.stateMachineService) {
983
+ this.stateMachineService.send({
984
+ type: TaskEvent.TRANSFER_CONFERENCE,
985
+ agentId: this.data.agentId,
986
+ });
987
+ }
988
+
989
+ const requestInteractionId =
990
+ this.data.interaction?.mainInteractionId || this.data.interactionId;
991
+
992
+ try {
993
+ LoggerProxy.info(`Transferring conference`, {
994
+ module: CC_FILE,
995
+ method: 'transferConference',
996
+ interactionId: requestInteractionId,
997
+ });
998
+
999
+ this.metricsManager.timeEvent([
1000
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_TRANSFER_SUCCESS,
1001
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_TRANSFER_FAILED,
1002
+ ]);
1003
+
1004
+ const response = await this.contact.conferenceTransfer({
1005
+ interactionId: requestInteractionId,
1006
+ });
1007
+
1008
+ // Send success event to transition state
1009
+ if (this.stateMachineService) {
1010
+ this.stateMachineService.send({
1011
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
1012
+ taskData: response.data,
1013
+ });
1014
+ }
1015
+
1016
+ this.metricsManager.trackEvent(
1017
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_TRANSFER_SUCCESS,
1018
+ {
1019
+ taskId: this.data.interactionId,
1020
+ requestInteractionId,
1021
+ agentId: this.data.agentId,
1022
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
1023
+ },
1024
+ ['operational', 'behavioral', 'business']
1025
+ );
1026
+
1027
+ LoggerProxy.log(`Successfully transferred conference`, {
1028
+ module: CC_FILE,
1029
+ method: 'transferConference',
1030
+ trackingId: response.trackingId,
1031
+ interactionId: requestInteractionId,
1032
+ });
1033
+
1034
+ return response;
1035
+ } catch (error) {
1036
+ // Send failure event
1037
+ if (this.stateMachineService) {
1038
+ this.stateMachineService.send({
1039
+ type: TaskEvent.TRANSFER_CONFERENCE_FAILED,
1040
+ reason: error.toString(),
1041
+ });
1042
+ }
1043
+
1044
+ const {error: detailedError} = getErrorDetails(error, 'transferConference', CC_FILE);
1045
+ this.metricsManager.trackEvent(
1046
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_TRANSFER_FAILED,
1047
+ {
1048
+ taskId: this.data.interactionId,
1049
+ requestInteractionId,
1050
+ agentId: this.data.agentId,
1051
+ error: error.toString(),
1052
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
1053
+ },
1054
+ ['operational', 'behavioral', 'business']
1055
+ );
1056
+
1057
+ LoggerProxy.error(`Failed to transfer conference`, {
1058
+ module: CC_FILE,
1059
+ method: 'transferConference',
1060
+ interactionId: requestInteractionId,
1061
+ });
1062
+
1063
+ throw detailedError;
1064
+ }
1065
+ }
1066
+
1067
+ /**
1068
+ * Toggle between consult call and main call during consulting.
1069
+ * If on consult leg (consultCallHeld = false), switches to main call by holding consult.
1070
+ * If on main call (consultCallHeld = true), switches to consult by resuming consult.
1071
+ *
1072
+ * @returns Promise<TaskResponse>
1073
+ * @throws Error if not in CONSULTING state or no consult media resource
1074
+ * @example
1075
+ * ```typescript
1076
+ * await task.switchCall();
1077
+ * ```
1078
+ */
1079
+ public async switchCall(): Promise<TaskResponse> {
1080
+ // Validate we're in CONSULTING state
1081
+ const state = this.getStateMachineSnapshot();
1082
+ if (!state?.matches(TaskState.CONSULTING)) {
1083
+ const currentState = state?.value as TaskState;
1084
+ const error = new Error(`Cannot switch call in ${currentState} state`);
1085
+ LoggerProxy.error('Switch call operation not allowed', {
1086
+ module: CC_FILE,
1087
+ method: 'switchCall',
1088
+ interactionId: this.data.interactionId,
1089
+ });
1090
+ throw error;
1091
+ }
1092
+
1093
+ // Validate we have a consult media resource
1094
+ const consultMediaResourceId = getConsultMediaResourceId(
1095
+ this.data.interaction,
1096
+ this.data.consultMediaResourceId,
1097
+ this.data.agentId
1098
+ );
1099
+ if (!consultMediaResourceId) {
1100
+ const error = new Error('No consult media resource available');
1101
+ LoggerProxy.error('Switch call failed - no consult media resource', {
1102
+ module: CC_FILE,
1103
+ method: 'switchCall',
1104
+ interactionId: this.data.interactionId,
1105
+ });
1106
+ throw error;
1107
+ }
1108
+
1109
+ const context = state.context;
1110
+ const isOnConsultLeg = !context.consultCallHeld;
1111
+
1112
+ // Determine direction and send appropriate state machine event
1113
+ const targetEvent = isOnConsultLeg
1114
+ ? TaskEvent.SWITCH_TO_MAIN_CALL
1115
+ : TaskEvent.SWITCH_TO_CONSULT;
1116
+ const revertEvent = isOnConsultLeg
1117
+ ? TaskEvent.SWITCH_TO_CONSULT
1118
+ : TaskEvent.SWITCH_TO_MAIN_CALL;
1119
+
1120
+ if (this.stateMachineService) {
1121
+ this.stateMachineService.send({type: targetEvent});
1122
+ }
1123
+
1124
+ this.metricsManager.timeEvent([
1125
+ METRIC_EVENT_NAMES.TASK_SWITCH_CALL_SUCCESS,
1126
+ METRIC_EVENT_NAMES.TASK_SWITCH_CALL_FAILED,
1127
+ ]);
1128
+
1129
+ try {
1130
+ if (isOnConsultLeg) {
1131
+ const response = await this.contact.unHold({
1132
+ interactionId: this.data.interactionId,
1133
+ data: {mediaResourceId: this.data.mediaResourceId},
1134
+ });
1135
+
1136
+ this.metricsManager.trackEvent(
1137
+ METRIC_EVENT_NAMES.TASK_SWITCH_CALL_SUCCESS,
1138
+ {
1139
+ taskId: this.data.interactionId,
1140
+ direction: 'toMainCall',
1141
+ mediaResourceId: consultMediaResourceId,
1142
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
1143
+ },
1144
+ ['operational', 'behavioral']
1145
+ );
1146
+
1147
+ LoggerProxy.log(`Switched to main call successfully`, {
1148
+ module: CC_FILE,
1149
+ method: 'switchCall',
1150
+ trackingId: response.trackingId,
1151
+ interactionId: this.data.interactionId,
1152
+ });
1153
+
1154
+ return response;
1155
+ }
1156
+
1157
+ const response = await this.contact.hold({
1158
+ interactionId: this.data.interactionId,
1159
+ data: {mediaResourceId: this.data.mediaResourceId},
1160
+ });
1161
+
1162
+ this.metricsManager.trackEvent(
1163
+ METRIC_EVENT_NAMES.TASK_SWITCH_CALL_SUCCESS,
1164
+ {
1165
+ taskId: this.data.interactionId,
1166
+ direction: 'toConsultCall',
1167
+ mediaResourceId: consultMediaResourceId,
1168
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
1169
+ },
1170
+ ['operational', 'behavioral']
1171
+ );
1172
+
1173
+ LoggerProxy.log(`Switched to consult call successfully`, {
1174
+ module: CC_FILE,
1175
+ method: 'switchCall',
1176
+ trackingId: response.trackingId,
1177
+ interactionId: this.data.interactionId,
1178
+ });
1179
+
1180
+ return response;
1181
+ } catch (error) {
1182
+ if (this.stateMachineService) {
1183
+ this.stateMachineService.send({type: revertEvent});
1184
+ }
1185
+
1186
+ this.metricsManager.trackEvent(
1187
+ METRIC_EVENT_NAMES.TASK_SWITCH_CALL_FAILED,
1188
+ {
1189
+ taskId: this.data.interactionId,
1190
+ direction: isOnConsultLeg ? 'toMainCall' : 'toConsultCall',
1191
+ mediaResourceId: consultMediaResourceId,
1192
+ error: error.toString(),
1193
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
1194
+ },
1195
+ ['operational', 'behavioral']
1196
+ );
1197
+
1198
+ const {error: detailedError} = getErrorDetails(error, 'switchCall', CC_FILE);
1199
+ LoggerProxy.error(`Failed to switch call`, {
1200
+ module: CC_FILE,
1201
+ method: 'switchCall',
1202
+ interactionId: this.data.interactionId,
1203
+ });
1204
+ throw detailedError;
1205
+ }
1206
+ }
1207
+
1208
+ protected override getChannelSpecificActionOverrides() {
1209
+ const baseOverrides = super.getChannelSpecificActionOverrides();
1210
+
1211
+ return {
1212
+ ...baseOverrides,
1213
+ emitTaskHold: this.createEmitSelfAction(TASK_EVENTS.TASK_HOLD, {updateTaskData: true}),
1214
+ emitTaskResume: this.createEmitSelfAction(TASK_EVENTS.TASK_RESUME, {updateTaskData: true}),
1215
+ emitTaskRecordingStarted: this.createEmitSelfAction(TASK_EVENTS.TASK_RECORDING_STARTED, {
1216
+ updateTaskData: true,
1217
+ }),
1218
+ emitTaskRecordingPaused: this.createEmitSelfAction(TASK_EVENTS.TASK_RECORDING_PAUSED, {
1219
+ updateTaskData: true,
1220
+ }),
1221
+ emitTaskRecordingPauseFailed: this.createEmitSelfAction(
1222
+ TASK_EVENTS.TASK_RECORDING_PAUSE_FAILED,
1223
+ {updateTaskData: true}
1224
+ ),
1225
+ emitTaskRecordingResumed: this.createEmitSelfAction(TASK_EVENTS.TASK_RECORDING_RESUMED, {
1226
+ updateTaskData: true,
1227
+ }),
1228
+ emitTaskRecordingResumeFailed: this.createEmitSelfAction(
1229
+ TASK_EVENTS.TASK_RECORDING_RESUME_FAILED,
1230
+ {updateTaskData: true}
1231
+ ),
1232
+ // Conference event emitters
1233
+ emitTaskParticipantJoined: this.createEmitSelfAction(TASK_EVENTS.TASK_PARTICIPANT_JOINED, {
1234
+ updateTaskData: true,
1235
+ }),
1236
+ emitTaskParticipantLeft: this.createEmitSelfAction(TASK_EVENTS.TASK_PARTICIPANT_LEFT, {
1237
+ updateTaskData: true,
1238
+ }),
1239
+ emitTaskConferenceStarted: this.createEmitSelfAction(TASK_EVENTS.TASK_CONFERENCE_STARTED, {
1240
+ updateTaskData: true,
1241
+ }),
1242
+ emitTaskConferenceEnded: this.createEmitSelfAction(TASK_EVENTS.TASK_CONFERENCE_ENDED, {
1243
+ updateTaskData: true,
1244
+ }),
1245
+ emitTaskConferenceFailed: this.createEmitSelfAction(TASK_EVENTS.TASK_CONFERENCE_FAILED, {
1246
+ updateTaskData: true,
1247
+ }),
1248
+ emitTaskExitConference: this.createEmitSelfAction(TASK_EVENTS.TASK_EXIT_CONFERENCE, {
1249
+ updateTaskData: false,
1250
+ }),
1251
+ emitTaskTransferConference: this.createEmitSelfAction(TASK_EVENTS.TASK_TRANSFER_CONFERENCE, {
1252
+ updateTaskData: false,
1253
+ }),
1254
+ emitTaskSwitchCall: this.createEmitSelfAction(TASK_EVENTS.TASK_SWITCH_CALL, {
1255
+ updateTaskData: false,
1256
+ }),
1257
+ emitTaskTransferConferenceFailed: this.createEmitSelfAction(
1258
+ TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED,
1259
+ {updateTaskData: true}
1260
+ ),
1261
+ emitTaskOutdialFailed: ({event}: TaskActionArgs) => {
1262
+ if (event && 'taskData' in event && event.taskData) {
1263
+ this.updateTaskData(event.taskData as TaskData);
1264
+ }
1265
+ const reason = (event as {reason?: string})?.reason || 'Outdial failed';
1266
+ this.emit(TASK_EVENTS.TASK_OUTDIAL_FAILED, reason);
1267
+ },
1268
+ };
1269
+ }
1270
+ }