@webex/contact-center 3.12.0-next.73 → 3.12.0-next.74

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