@webex/contact-center 3.11.0 → 3.12.0-llmrefactor.2

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,738 @@
1
+ /**
2
+ * Task State Machine Actions - Action implementations executed during state transitions
3
+ */
4
+
5
+ import {assign} from 'xstate';
6
+ import {
7
+ TaskContext,
8
+ TaskEventPayload,
9
+ UIControlConfig,
10
+ TaskActionsMap,
11
+ TaskActionArgs,
12
+ RecordingStateUpdate,
13
+ } from './types';
14
+ import {
15
+ TaskEvent,
16
+ TaskState,
17
+ INTERACTION_STATE,
18
+ CONSULT_STATE,
19
+ MEDIA_TYPE_CONSULT,
20
+ } from './constants';
21
+ import {DestinationType, TaskData} from '../types';
22
+ import {CC_EVENTS} from '../../config/types';
23
+ import {computeUIControls, getDefaultUIControls} from './uiControlsComputer';
24
+ import {getIsConferenceInProgress, isEpDnConsultPendingConferenceMerge} from '../TaskUtils';
25
+ import {hasActiveConsultInPostCall} from './guards';
26
+
27
+ /** Terminal consult events that clear consult context in deriveTaskDataUpdates. */
28
+ const CONSULT_CONTEXT_CLEAR_EVENTS = new Set<string>([
29
+ CC_EVENTS.AGENT_CONSULT_ENDED,
30
+ CC_EVENTS.AGENT_CONSULT_FAILED,
31
+ ]);
32
+
33
+ const determineConsultInitiator = (
34
+ taskData: TaskData | undefined,
35
+ selfAgentId: string | undefined
36
+ ): boolean | undefined => {
37
+ if (!selfAgentId) return undefined;
38
+
39
+ if (taskData?.consultingAgentId) {
40
+ return taskData.consultingAgentId === selfAgentId;
41
+ }
42
+
43
+ if (taskData?.isConsulted === true) return false;
44
+
45
+ return undefined;
46
+ };
47
+
48
+ const deriveRecordingState = (taskData?: TaskData | null): RecordingStateUpdate => {
49
+ const callProcessingDetails = taskData?.interaction?.callProcessingDetails;
50
+
51
+ if (!callProcessingDetails) {
52
+ return {};
53
+ }
54
+
55
+ const update: RecordingStateUpdate = {};
56
+ const {recordingStarted, recordInProgress, isPaused} = callProcessingDetails as {
57
+ recordingStarted?: boolean;
58
+ recordInProgress?: boolean;
59
+ isPaused?: boolean;
60
+ };
61
+
62
+ if (recordingStarted !== undefined) {
63
+ update.recordingControlsAvailable = recordingStarted;
64
+ if (!recordingStarted) {
65
+ update.recordingInProgress = false;
66
+ }
67
+ }
68
+
69
+ if (recordInProgress !== undefined) {
70
+ update.recordingControlsAvailable = recordInProgress || recordingStarted || false;
71
+ update.recordingInProgress = recordInProgress;
72
+ }
73
+
74
+ if (
75
+ update.recordingControlsAvailable === undefined &&
76
+ update.recordingInProgress === undefined &&
77
+ recordingStarted
78
+ ) {
79
+ update.recordingControlsAvailable = true;
80
+ update.recordingInProgress = true;
81
+ }
82
+
83
+ if (isPaused !== undefined) {
84
+ update.recordingControlsAvailable = true;
85
+ update.recordingInProgress = !isPaused;
86
+ }
87
+
88
+ return update;
89
+ };
90
+
91
+ const isActiveConsultState = (taskData: TaskData | undefined, selfAgentId?: string): boolean => {
92
+ if (taskData?.interaction?.state === INTERACTION_STATE.CONSULTING) return true;
93
+ if (selfAgentId) {
94
+ const selfParticipant = taskData?.interaction?.participants?.[selfAgentId] as any;
95
+ const hasConsultMedia = Object.values(taskData?.interaction?.media ?? {}).some(
96
+ (media: any) => media?.mType === MEDIA_TYPE_CONSULT
97
+ );
98
+ // Pending consult before destination joins stays HELD, not CONSULTING.
99
+ if (
100
+ selfParticipant?.consultState === 'consultInitiated' &&
101
+ !hasJoinedConsultDestination(taskData, selfAgentId) &&
102
+ hasConsultMedia &&
103
+ taskData?.isConsulted === false
104
+ ) {
105
+ return false;
106
+ }
107
+ if (
108
+ (selfParticipant?.consultState === CONSULT_STATE.CONSULTING ||
109
+ selfParticipant?.consultState === CONSULT_STATE.CONFERENCING) &&
110
+ hasConsultMedia &&
111
+ taskData?.isConsulted === false
112
+ ) {
113
+ return true;
114
+ }
115
+ }
116
+ if (taskData?.interaction?.state === INTERACTION_STATE.POST_CALL && selfAgentId) {
117
+ const selfParticipant = taskData.interaction?.participants?.[selfAgentId] as any;
118
+ const hasConsultMedia = Object.values(taskData.interaction?.media ?? {}).some(
119
+ (media: any) => media?.mType === MEDIA_TYPE_CONSULT
120
+ );
121
+ if (selfParticipant?.consultState === CONSULT_STATE.CONSULTING && hasConsultMedia) return true;
122
+ }
123
+
124
+ return false;
125
+ };
126
+
127
+ const isSelfConsultingOrPending = (
128
+ taskData: TaskData | undefined,
129
+ selfAgentId?: string
130
+ ): boolean => {
131
+ if (!taskData || !selfAgentId) return false;
132
+ const selfParticipant = taskData?.interaction?.participants?.[selfAgentId] as any;
133
+
134
+ return (
135
+ selfParticipant?.consultState === CONSULT_STATE.CONSULTING ||
136
+ selfParticipant?.consultState === CONSULT_STATE.CONFERENCING ||
137
+ selfParticipant?.consultState === 'consultInitiated'
138
+ );
139
+ };
140
+
141
+ const isDnOnConsultMedia = (taskData: TaskData | undefined, selfAgentId?: string): boolean => {
142
+ if (!taskData?.interaction?.participants) return false;
143
+
144
+ const consultMedia: any = taskData.consultMediaResourceId
145
+ ? taskData.interaction.media?.[taskData.consultMediaResourceId]
146
+ : Object.values(taskData.interaction.media ?? {}).find(
147
+ (m: any) => m?.mType === MEDIA_TYPE_CONSULT
148
+ );
149
+ const consultParticipantIds = new Set(consultMedia?.participants ?? []);
150
+ if (consultParticipantIds.size === 0) return false;
151
+
152
+ return Object.values(taskData.interaction.participants).some((p: any) => {
153
+ if (!p || p.hasLeft || p.id === selfAgentId) return false;
154
+ if (!consultParticipantIds.has(p.id)) return false;
155
+ const pType = String(p.pType ?? '').toUpperCase();
156
+
157
+ return pType === 'DN' || pType === 'EP-DN' || pType === 'EP_DN';
158
+ });
159
+ };
160
+
161
+ const mapConsultDestinationType = (
162
+ destinationType: string | undefined
163
+ ): DestinationType | undefined => {
164
+ if (!destinationType) return undefined;
165
+ const normalized = String(destinationType).toUpperCase();
166
+ if (normalized === 'DN' || normalized === 'EP-DN' || normalized === 'EP_DN') {
167
+ return 'entryPoint' as DestinationType;
168
+ }
169
+
170
+ return destinationType as DestinationType;
171
+ };
172
+
173
+ const hasJoinedConsultDestination = (
174
+ taskData: TaskData | undefined,
175
+ selfAgentId?: string
176
+ ): boolean => {
177
+ if (!taskData?.interaction) return false;
178
+ const participants = taskData.interaction.participants as any;
179
+ const cpd = taskData.interaction.callProcessingDetails as any;
180
+ const backendSaysJoined = cpd?.consultDestinationAgentJoined === 'true';
181
+ if (backendSaysJoined) return true;
182
+ if (!participants) return false;
183
+
184
+ const effectiveSelfAgentId = selfAgentId ?? taskData.agentId;
185
+ const selfParticipant = effectiveSelfAgentId
186
+ ? (participants[effectiveSelfAgentId] as {consultState?: string} | undefined)
187
+ : undefined;
188
+
189
+ if (
190
+ taskData.type === 'AgentConsulting' &&
191
+ selfParticipant?.consultState === CONSULT_STATE.CONSULTING &&
192
+ taskData.isConsulted === false &&
193
+ isDnOnConsultMedia(taskData, effectiveSelfAgentId)
194
+ ) {
195
+ return true;
196
+ }
197
+
198
+ return Object.values(participants).some((p: any) => {
199
+ if (!p || p.isConsulted !== true || p.hasLeft) return false;
200
+
201
+ return p.hasJoined === true || p.consultState === CONSULT_STATE.CONSULTING;
202
+ });
203
+ };
204
+
205
+ const deriveConsultCallHeldFromTaskData = (taskData: TaskData | undefined): boolean | undefined => {
206
+ if (!taskData?.interaction) return undefined;
207
+
208
+ const eventType = taskData.type;
209
+ if (eventType === 'AgentContactHeld') return true;
210
+ if (eventType === 'AgentContactUnheld') return false;
211
+
212
+ const consultMediaId = taskData.consultMediaResourceId;
213
+
214
+ const consultMedia: any = consultMediaId
215
+ ? taskData.interaction.media?.[consultMediaId]
216
+ : Object.values(taskData.interaction.media ?? {}).find(
217
+ (m: any) => m?.mType === MEDIA_TYPE_CONSULT
218
+ );
219
+
220
+ if (!consultMedia) return undefined;
221
+
222
+ return Boolean(consultMedia.isHold);
223
+ };
224
+
225
+ export const getTaskStateForUiControls = (
226
+ taskData: TaskData | undefined,
227
+ selfAgentId: string | undefined
228
+ ): TaskState => {
229
+ if (!taskData?.interaction) {
230
+ return TaskState.IDLE;
231
+ }
232
+
233
+ if (taskData.interaction.isTerminated === true) {
234
+ return TaskState.WRAPPING_UP;
235
+ }
236
+
237
+ // EP-DN consult merged to conference before Agent 2 joins main — UI is conferencing (CAI-8329).
238
+ if (isEpDnConsultPendingConferenceMerge(taskData, selfAgentId)) {
239
+ return TaskState.CONFERENCING;
240
+ }
241
+
242
+ if (isActiveConsultState(taskData, selfAgentId)) {
243
+ return TaskState.CONSULTING;
244
+ }
245
+
246
+ if (
247
+ taskData.interaction.state === INTERACTION_STATE.CONFERENCE ||
248
+ getIsConferenceInProgress(taskData)
249
+ ) {
250
+ return TaskState.CONFERENCING;
251
+ }
252
+
253
+ const mainMediaId = taskData.interaction.mainInteractionId || taskData.interactionId;
254
+ const isMainHeld = Boolean(
255
+ mainMediaId && taskData.interaction.media?.[mainMediaId]?.isHold === true
256
+ );
257
+ if (taskData.interaction.state === 'hold' || isMainHeld) {
258
+ return TaskState.HELD;
259
+ }
260
+
261
+ return TaskState.CONNECTED;
262
+ };
263
+
264
+ const deriveTaskDataUpdates = (context: TaskContext, taskData: TaskData | undefined) =>
265
+ taskData
266
+ ? (() => {
267
+ const enrichedTaskData: TaskData =
268
+ taskData.type === 'AgentConsultCreated' && taskData.agentId
269
+ ? {...taskData, consultingAgentId: taskData.agentId}
270
+ : taskData;
271
+
272
+ const updates: Partial<TaskContext> = {
273
+ taskData: enrichedTaskData,
274
+ ...deriveRecordingState(enrichedTaskData),
275
+ };
276
+
277
+ const selfAgentId = context.uiControlConfig.agentId ?? enrichedTaskData?.agentId;
278
+ const consultingActive =
279
+ isActiveConsultState(enrichedTaskData, selfAgentId) ||
280
+ hasActiveConsultInPostCall(enrichedTaskData, selfAgentId);
281
+ const conferenceFromPayload =
282
+ enrichedTaskData?.interaction?.state === INTERACTION_STATE.CONFERENCE ||
283
+ getIsConferenceInProgress(enrichedTaskData);
284
+ const selfConsultingOrPending = isSelfConsultingOrPending(enrichedTaskData, selfAgentId);
285
+ const inferredConsultingInitiator =
286
+ selfConsultingOrPending && enrichedTaskData?.isConsulted === false;
287
+
288
+ if (enrichedTaskData.destAgentId) {
289
+ const isEpDnWithStoredId =
290
+ context.consultDestinationType === 'entryPoint' && context.consultDestinationAgentId;
291
+ if (!isEpDnWithStoredId) {
292
+ updates.consultDestinationAgentId = enrichedTaskData.destAgentId;
293
+ }
294
+ }
295
+
296
+ const isConsultTerminalEvent =
297
+ enrichedTaskData.type != null && CONSULT_CONTEXT_CLEAR_EVENTS.has(enrichedTaskData.type);
298
+ const epDnPendingMergeActive = isEpDnConsultPendingConferenceMerge(
299
+ enrichedTaskData,
300
+ selfAgentId
301
+ );
302
+
303
+ if (isConsultTerminalEvent && !epDnPendingMergeActive) {
304
+ updates.consultInitiator = false;
305
+ updates.consultFromConference = false;
306
+ updates.consultDestinationAgentJoined = false;
307
+ updates.consultCallHeld = false;
308
+ updates.consultDestinationType = null;
309
+ updates.consultDestinationAgentId = null;
310
+ updates.transferConferenceRequested = false;
311
+ updates.hideBlindTransferForEpDnPendingMerge = false;
312
+ }
313
+
314
+ updates.hideBlindTransferForEpDnPendingMerge = isEpDnConsultPendingConferenceMerge(
315
+ enrichedTaskData,
316
+ selfAgentId
317
+ );
318
+
319
+ if (consultingActive && enrichedTaskData.destinationType) {
320
+ updates.consultDestinationType = mapConsultDestinationType(
321
+ enrichedTaskData.destinationType
322
+ );
323
+ }
324
+
325
+ if (!isConsultTerminalEvent) {
326
+ const consultInitiator = determineConsultInitiator(enrichedTaskData, selfAgentId);
327
+ if (consultInitiator !== undefined) {
328
+ updates.consultInitiator = consultInitiator;
329
+ } else if (
330
+ enrichedTaskData.type === 'AgentConsultCreated' &&
331
+ enrichedTaskData.agentId &&
332
+ enrichedTaskData.agentId !== selfAgentId &&
333
+ conferenceFromPayload
334
+ ) {
335
+ updates.consultInitiator = false;
336
+ } else if (!context.consultInitiator) {
337
+ if (
338
+ inferredConsultingInitiator ||
339
+ (consultingActive && enrichedTaskData.isConsulted === false)
340
+ ) {
341
+ updates.consultInitiator = true;
342
+ }
343
+ }
344
+ }
345
+
346
+ const effectiveConsultInitiator = updates.consultInitiator ?? context.consultInitiator;
347
+ if (
348
+ !isConsultTerminalEvent &&
349
+ effectiveConsultInitiator &&
350
+ conferenceFromPayload &&
351
+ (consultingActive ||
352
+ selfConsultingOrPending ||
353
+ Boolean(enrichedTaskData?.consultMediaResourceId))
354
+ ) {
355
+ updates.consultFromConference = true;
356
+ }
357
+
358
+ if (
359
+ !isConsultTerminalEvent &&
360
+ (consultingActive || selfConsultingOrPending) &&
361
+ enrichedTaskData.interaction
362
+ ) {
363
+ const joinedConsultee = hasJoinedConsultDestination(enrichedTaskData, selfAgentId);
364
+ const selfParticipant = selfAgentId
365
+ ? (enrichedTaskData.interaction.participants?.[selfAgentId] as
366
+ | {consultState?: string}
367
+ | undefined)
368
+ : undefined;
369
+ const agentConsultingSelfJoined =
370
+ enrichedTaskData.type === 'AgentConsulting' &&
371
+ effectiveConsultInitiator &&
372
+ selfParticipant?.consultState === CONSULT_STATE.CONSULTING;
373
+
374
+ if (joinedConsultee || agentConsultingSelfJoined) {
375
+ updates.consultDestinationAgentJoined = true;
376
+ } else if (selfParticipant?.consultState === 'consultInitiated') {
377
+ updates.consultDestinationAgentJoined = false;
378
+ }
379
+
380
+ if (!context.consultDestinationType && !updates.consultDestinationType) {
381
+ const hasEpDnParticipant = Boolean(
382
+ enrichedTaskData.interaction.participants &&
383
+ Object.values(enrichedTaskData.interaction.participants).some((p: any) => {
384
+ if (p?.hasLeft) return false;
385
+ const pType = String(p?.pType ?? '').toUpperCase();
386
+
387
+ return pType === 'EP-DN' || pType === 'EP_DN' || pType === 'DN';
388
+ })
389
+ );
390
+ if (hasEpDnParticipant) updates.consultDestinationType = 'entryPoint' as any;
391
+ }
392
+
393
+ if (effectiveConsultInitiator) {
394
+ const consultCallHeld = deriveConsultCallHeldFromTaskData(enrichedTaskData);
395
+ if (consultCallHeld !== undefined) {
396
+ updates.consultCallHeld = consultCallHeld;
397
+ }
398
+ }
399
+ }
400
+
401
+ const nextContext = {
402
+ ...context,
403
+ ...updates,
404
+ } as TaskContext;
405
+ const inferredState = getTaskStateForUiControls(enrichedTaskData, selfAgentId);
406
+
407
+ updates.uiControls = computeUIControls(inferredState, nextContext, enrichedTaskData);
408
+
409
+ return updates;
410
+ })()
411
+ : {};
412
+
413
+ const getTaskDataFromEvent = (event?: TaskEventPayload): TaskData | undefined =>
414
+ event && typeof event === 'object' ? (event as any).taskData : undefined;
415
+
416
+ export function createInitialContext(
417
+ uiControlConfig: UIControlConfig,
418
+ initialState: TaskState = TaskState.IDLE
419
+ ): TaskContext {
420
+ const baseContext: TaskContext = {
421
+ taskData: null,
422
+ consultInitiator: false,
423
+ exitingConference: false,
424
+ consultFromConference: false,
425
+ transferConferenceRequested: false,
426
+ consultDestinationType: null,
427
+ consultDestinationAgentId: null,
428
+ consultDestinationAgentJoined: false,
429
+ consultCallHeld: false,
430
+ hideBlindTransferForEpDnPendingMerge: false,
431
+ recordingControlsAvailable: false,
432
+ recordingInProgress: false,
433
+ uiControlConfig,
434
+ uiControls: getDefaultUIControls(),
435
+ };
436
+
437
+ baseContext.uiControls = computeUIControls(initialState, baseContext);
438
+
439
+ return baseContext;
440
+ }
441
+
442
+ export function updateUIControls(currentState: TaskState) {
443
+ return assign(({context}: TaskActionArgs) => ({
444
+ uiControls: computeUIControls(currentState, context, context.taskData ?? undefined),
445
+ }));
446
+ }
447
+
448
+ export const actions: TaskActionsMap = {
449
+ initializeTask: assign(({context, event}: {context: TaskContext; event: TaskEventPayload}) => {
450
+ return {
451
+ consultInitiator: false,
452
+ exitingConference: false,
453
+ consultDestinationType: null,
454
+ consultDestinationAgentJoined: false,
455
+ ...deriveTaskDataUpdates(context, getTaskDataFromEvent(event)),
456
+ };
457
+ }),
458
+
459
+ /**
460
+ * Update task data from ASSIGN event
461
+ */
462
+ updateTaskData: assign(({context, event}: TaskActionArgs) => {
463
+ return deriveTaskDataUpdates(context, getTaskDataFromEvent(event));
464
+ }),
465
+
466
+ /**
467
+ * Set consult initiator flag
468
+ *
469
+ * IMPORTANT: This action is called for CONSULT (user action) and CONSULT_CREATED (backend event).
470
+ * For CONSULT (user action): The user explicitly clicked Consult, so they ARE the initiator.
471
+ * For CONSULT_CREATED (backend event): Check taskData.isConsulted to determine if this agent
472
+ * is the initiator. If isConsulted === true, this is Agent B (the consulted party), NOT the initiator.
473
+ *
474
+ * This prevents all agents in a conference from becoming consultInitiator when one agent
475
+ * starts a new consult.
476
+ */
477
+ setConsultInitiator: assign(({event}: TaskActionArgs) => {
478
+ const taskData = getTaskDataFromEvent(event);
479
+
480
+ if (event.type === TaskEvent.CONSULT) return {consultInitiator: true};
481
+
482
+ const selfAgentId = taskData?.agentId;
483
+ const consultInitiator = determineConsultInitiator(taskData, selfAgentId);
484
+ if (consultInitiator === true) return {consultInitiator: true};
485
+ if (consultInitiator === false) return {consultInitiator: false};
486
+
487
+ return {};
488
+ }),
489
+
490
+ handleConsultFailed: assign(({context, event}: TaskActionArgs) => {
491
+ const taskData = getTaskDataFromEvent(event) ?? context.taskData;
492
+ const selfAgentId = context.uiControlConfig.agentId ?? taskData?.agentId;
493
+ const hideBlindTransferForEpDnPendingMerge = isEpDnConsultPendingConferenceMerge(
494
+ taskData,
495
+ selfAgentId
496
+ );
497
+ const cleared = {
498
+ consultDestinationType: null,
499
+ consultDestinationAgentId: null,
500
+ consultDestinationAgentJoined: false,
501
+ consultInitiator: false,
502
+ exitingConference: false,
503
+ consultCallHeld: false,
504
+ consultFromConference: false,
505
+ transferConferenceRequested: false,
506
+ hideBlindTransferForEpDnPendingMerge,
507
+ };
508
+ const nextContext = {...context, ...cleared, taskData} as TaskContext;
509
+ const inferredState = getTaskStateForUiControls(taskData, selfAgentId);
510
+
511
+ return {
512
+ ...cleared,
513
+ uiControls: computeUIControls(inferredState, nextContext, taskData),
514
+ };
515
+ }),
516
+ handleConferenceStarted: assign({consultInitiator: false}),
517
+
518
+ setConsultDestination: assign(({event}: TaskActionArgs) => {
519
+ if (!event || event.type !== TaskEvent.CONSULT) {
520
+ return {};
521
+ }
522
+
523
+ const taskData = getTaskDataFromEvent(event);
524
+ const consultDestinationType =
525
+ 'destinationType' in event ? event.destinationType ?? null : null;
526
+ const consultDestinationAgentId =
527
+ ('destAgentId' in event ? event.destAgentId : null) ??
528
+ ('destination' in event ? (event as any).destination : null) ??
529
+ null;
530
+
531
+ return {
532
+ consultDestinationType,
533
+ consultDestinationAgentId,
534
+ consultDestinationAgentJoined: false,
535
+ consultFromConference: false,
536
+ taskData: {
537
+ ...taskData,
538
+ destAgentId: consultDestinationAgentId,
539
+ },
540
+ };
541
+ }),
542
+
543
+ setConsultFromConference: assign({consultFromConference: true}),
544
+
545
+ forceConsultInitiator: assign({consultInitiator: true}),
546
+
547
+ setConsultAgentJoined: assign(
548
+ ({context, event}: {context: TaskContext; event: TaskEventPayload}) => {
549
+ if (context.consultDestinationAgentJoined) {
550
+ return {};
551
+ }
552
+
553
+ if (
554
+ !event ||
555
+ event.type !== TaskEvent.CONSULTING_ACTIVE ||
556
+ !('consultDestinationAgentJoined' in event)
557
+ ) {
558
+ return {};
559
+ }
560
+
561
+ const eventValue = (event as {consultDestinationAgentJoined: boolean})
562
+ .consultDestinationAgentJoined;
563
+
564
+ return eventValue ? {consultDestinationAgentJoined: true} : {};
565
+ }
566
+ ),
567
+
568
+ setRecordingState: assign(({event}: TaskActionArgs) => {
569
+ if (!event || !('type' in event)) {
570
+ return {};
571
+ }
572
+
573
+ if (event.type === TaskEvent.PAUSE_RECORDING) {
574
+ return {
575
+ recordingControlsAvailable: true,
576
+ recordingInProgress: false,
577
+ };
578
+ }
579
+ if (event.type === TaskEvent.RESUME_RECORDING) {
580
+ return {
581
+ recordingControlsAvailable: true,
582
+ recordingInProgress: true,
583
+ };
584
+ }
585
+
586
+ return {};
587
+ }),
588
+
589
+ clearConsultState: assign(({context, event}: TaskActionArgs) => {
590
+ const taskData = context.taskData ?? getTaskDataFromEvent(event);
591
+ const selfAgentId = context.uiControlConfig.agentId ?? taskData?.agentId;
592
+ const hideBlindTransferForEpDnPendingMerge = isEpDnConsultPendingConferenceMerge(
593
+ taskData,
594
+ selfAgentId
595
+ );
596
+ const cleared = {
597
+ consultDestinationType: null,
598
+ consultDestinationAgentId: null,
599
+ consultDestinationAgentJoined: false,
600
+ consultInitiator: false,
601
+ exitingConference: false,
602
+ consultCallHeld: false,
603
+ consultFromConference: false,
604
+ transferConferenceRequested: false,
605
+ hideBlindTransferForEpDnPendingMerge,
606
+ };
607
+ const nextContext = {...context, ...cleared, taskData} as TaskContext;
608
+ const inferredState = getTaskStateForUiControls(taskData, selfAgentId);
609
+
610
+ return {
611
+ ...cleared,
612
+ uiControls: computeUIControls(inferredState, nextContext, taskData),
613
+ };
614
+ }),
615
+
616
+ setTransferConferenceRequested: assign({transferConferenceRequested: true}),
617
+ clearTransferConferenceRequested: assign({transferConferenceRequested: false}),
618
+
619
+ setConsultCallHeld: assign({consultCallHeld: true}),
620
+ clearConsultCallHeld: assign({consultCallHeld: false}),
621
+ handleSwitchToMainCall: assign({consultCallHeld: true}),
622
+ handleSwitchToConsult: assign({consultCallHeld: false}),
623
+
624
+ handleConferenceFailed: assign(({event}: TaskActionArgs) => {
625
+ const taskData = getTaskDataFromEvent(event);
626
+
627
+ return taskData ? {taskData} : {};
628
+ }),
629
+ handleParticipantLeft: assign(({event}: TaskActionArgs) => {
630
+ const taskData = getTaskDataFromEvent(event);
631
+
632
+ return taskData ? {taskData} : {};
633
+ }),
634
+
635
+ setExitingConference: assign({exitingConference: true}),
636
+
637
+ handleTransferConferenceSuccess: assign(({event}: TaskActionArgs) => {
638
+ const taskData = getTaskDataFromEvent(event);
639
+
640
+ return taskData ? {taskData} : {};
641
+ }),
642
+
643
+ setHoldState: assign(({context, event}: TaskActionArgs) => {
644
+ if (
645
+ !event ||
646
+ (event.type !== TaskEvent.HOLD_SUCCESS && event.type !== TaskEvent.UNHOLD_SUCCESS)
647
+ ) {
648
+ return {};
649
+ }
650
+
651
+ const mediaResourceId =
652
+ 'mediaResourceId' in event
653
+ ? (event as {mediaResourceId?: string}).mediaResourceId
654
+ : undefined;
655
+
656
+ if (!mediaResourceId) {
657
+ return {};
658
+ }
659
+
660
+ const interaction = context.taskData?.interaction;
661
+ const mediaEntry = interaction?.media?.[mediaResourceId];
662
+
663
+ if (!interaction || !mediaEntry) {
664
+ return {};
665
+ }
666
+
667
+ const updatedMedia = {
668
+ ...interaction.media,
669
+ [mediaResourceId]: {
670
+ ...mediaEntry,
671
+ isHold: event.type === TaskEvent.HOLD_SUCCESS,
672
+ },
673
+ };
674
+
675
+ const updates: Partial<TaskContext> = {
676
+ taskData: {
677
+ ...(context.taskData as TaskData),
678
+ interaction: {
679
+ ...interaction,
680
+ media: updatedMedia,
681
+ },
682
+ },
683
+ };
684
+
685
+ const consultMediaId = context.taskData?.consultMediaResourceId;
686
+ const isConsultMedia = consultMediaId
687
+ ? mediaResourceId === consultMediaId
688
+ : mediaEntry?.mType === 'consult';
689
+
690
+ if (isConsultMedia && context.consultInitiator) {
691
+ updates.consultCallHeld = event.type === TaskEvent.HOLD_SUCCESS;
692
+ }
693
+
694
+ return updates;
695
+ }),
696
+
697
+ markEnded: assign(() => ({
698
+ recordingControlsAvailable: false,
699
+ recordingInProgress: false,
700
+ })),
701
+
702
+ requestAutoAnswer: () => undefined,
703
+ requestCleanup: () => undefined,
704
+ cleanupResources: () => undefined,
705
+
706
+ // Event emitters - placeholders overridden by consumers
707
+ emitTaskCampaignPreviewReservation: () => undefined,
708
+ emitTaskCampaignPreviewAcceptFailed: () => undefined,
709
+ emitTaskCampaignPreviewSkipFailed: () => undefined,
710
+ emitTaskCampaignPreviewRemoveFailed: () => undefined,
711
+ emitTaskIncoming: () => undefined,
712
+ emitTaskHydrate: () => undefined,
713
+ emitTaskOfferContact: () => undefined,
714
+ emitTaskAssigned: () => undefined,
715
+ emitTaskHold: () => undefined,
716
+ emitTaskResume: () => undefined,
717
+ emitTaskEnd: () => undefined,
718
+ emitTaskOfferConsult: () => undefined,
719
+ emitTaskConsultCreated: () => undefined,
720
+ emitTaskConsulting: () => undefined,
721
+ emitTaskConsultAccepted: () => undefined,
722
+ emitTaskConsultEnd: () => undefined,
723
+ emitTaskReject: () => undefined,
724
+ emitTaskWrapup: () => undefined,
725
+ emitTaskRecordingStarted: () => undefined,
726
+ emitTaskRecordingPaused: () => undefined,
727
+ emitTaskRecordingResumed: () => undefined,
728
+ emitTaskWrappedup: () => undefined,
729
+ emitTaskParticipantLeft: () => undefined,
730
+ emitTaskConferenceStarted: () => undefined,
731
+ emitTaskConferenceEnded: () => undefined,
732
+ emitTaskExitConference: () => undefined,
733
+ emitTaskTransferConference: () => undefined,
734
+ emitTaskSwitchCall: () => undefined,
735
+ emitTaskConferenceFailed: () => undefined,
736
+ emitTaskTransferConferenceFailed: () => undefined,
737
+ emitTaskOutdialFailed: () => undefined,
738
+ };