@webex/contact-center 3.12.0-next.9 → 3.12.0-task-refactor.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 (200) hide show
  1. package/AGENTS.md +438 -0
  2. package/ai-docs/README.md +131 -0
  3. package/ai-docs/RULES.md +455 -0
  4. package/ai-docs/patterns/event-driven-patterns.md +485 -0
  5. package/ai-docs/patterns/testing-patterns.md +480 -0
  6. package/ai-docs/patterns/typescript-patterns.md +365 -0
  7. package/ai-docs/templates/README.md +102 -0
  8. package/ai-docs/templates/documentation/create-agents-md.md +240 -0
  9. package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
  10. package/ai-docs/templates/existing-service/bug-fix.md +254 -0
  11. package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
  12. package/ai-docs/templates/new-method/00-master.md +80 -0
  13. package/ai-docs/templates/new-method/01-requirements.md +232 -0
  14. package/ai-docs/templates/new-method/02-implementation.md +295 -0
  15. package/ai-docs/templates/new-method/03-tests.md +201 -0
  16. package/ai-docs/templates/new-method/04-validation.md +141 -0
  17. package/ai-docs/templates/new-service/00-master.md +109 -0
  18. package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
  19. package/ai-docs/templates/new-service/02-code-generation.md +346 -0
  20. package/ai-docs/templates/new-service/03-integration.md +178 -0
  21. package/ai-docs/templates/new-service/04-test-generation.md +205 -0
  22. package/ai-docs/templates/new-service/05-validation.md +145 -0
  23. package/dist/cc.js +65 -123
  24. package/dist/cc.js.map +1 -1
  25. package/dist/constants.js +13 -2
  26. package/dist/constants.js.map +1 -1
  27. package/dist/index.js +13 -5
  28. package/dist/index.js.map +1 -1
  29. package/dist/metrics/behavioral-events.js +26 -13
  30. package/dist/metrics/behavioral-events.js.map +1 -1
  31. package/dist/metrics/constants.js +7 -6
  32. package/dist/metrics/constants.js.map +1 -1
  33. package/dist/services/ApiAiAssistant.js +0 -3
  34. package/dist/services/ApiAiAssistant.js.map +1 -1
  35. package/dist/services/config/Util.js +2 -3
  36. package/dist/services/config/Util.js.map +1 -1
  37. package/dist/services/config/types.js +16 -14
  38. package/dist/services/config/types.js.map +1 -1
  39. package/dist/services/constants.js +0 -1
  40. package/dist/services/constants.js.map +1 -1
  41. package/dist/services/core/Err.js.map +1 -1
  42. package/dist/services/core/Utils.js +79 -55
  43. package/dist/services/core/Utils.js.map +1 -1
  44. package/dist/services/core/aqm-reqs.js +17 -92
  45. package/dist/services/core/aqm-reqs.js.map +1 -1
  46. package/dist/services/core/websocket/WebSocketManager.js +5 -25
  47. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  48. package/dist/services/core/websocket/types.js.map +1 -1
  49. package/dist/services/index.js +1 -2
  50. package/dist/services/index.js.map +1 -1
  51. package/dist/services/task/Task.js +644 -0
  52. package/dist/services/task/Task.js.map +1 -0
  53. package/dist/services/task/TaskFactory.js +45 -0
  54. package/dist/services/task/TaskFactory.js.map +1 -0
  55. package/dist/services/task/TaskManager.js +570 -535
  56. package/dist/services/task/TaskManager.js.map +1 -1
  57. package/dist/services/task/TaskUtils.js +132 -28
  58. package/dist/services/task/TaskUtils.js.map +1 -1
  59. package/dist/services/task/constants.js +7 -6
  60. package/dist/services/task/constants.js.map +1 -1
  61. package/dist/services/task/dialer.js +0 -51
  62. package/dist/services/task/dialer.js.map +1 -1
  63. package/dist/services/task/digital/Digital.js +77 -0
  64. package/dist/services/task/digital/Digital.js.map +1 -0
  65. package/dist/services/task/state-machine/TaskStateMachine.js +634 -0
  66. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
  67. package/dist/services/task/state-machine/actions.js +372 -0
  68. package/dist/services/task/state-machine/actions.js.map +1 -0
  69. package/dist/services/task/state-machine/constants.js +139 -0
  70. package/dist/services/task/state-machine/constants.js.map +1 -0
  71. package/dist/services/task/state-machine/guards.js +263 -0
  72. package/dist/services/task/state-machine/guards.js.map +1 -0
  73. package/dist/services/task/state-machine/index.js +53 -0
  74. package/dist/services/task/state-machine/index.js.map +1 -0
  75. package/dist/services/task/state-machine/types.js +54 -0
  76. package/dist/services/task/state-machine/types.js.map +1 -0
  77. package/dist/services/task/state-machine/uiControlsComputer.js +377 -0
  78. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
  79. package/dist/services/task/taskDataNormalizer.js +99 -0
  80. package/dist/services/task/taskDataNormalizer.js.map +1 -0
  81. package/dist/services/task/types.js +157 -18
  82. package/dist/services/task/types.js.map +1 -1
  83. package/dist/services/task/voice/Voice.js +1031 -0
  84. package/dist/services/task/voice/Voice.js.map +1 -0
  85. package/dist/services/task/voice/WebRTC.js +149 -0
  86. package/dist/services/task/voice/WebRTC.js.map +1 -0
  87. package/dist/types/cc.d.ts +4 -33
  88. package/dist/types/constants.d.ts +13 -2
  89. package/dist/types/index.d.ts +11 -5
  90. package/dist/types/metrics/constants.d.ts +5 -3
  91. package/dist/types/services/ApiAiAssistant.d.ts +1 -1
  92. package/dist/types/services/config/types.d.ts +97 -25
  93. package/dist/types/services/core/Err.d.ts +0 -2
  94. package/dist/types/services/core/Utils.d.ts +25 -23
  95. package/dist/types/services/core/aqm-reqs.d.ts +0 -49
  96. package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -1
  97. package/dist/types/services/core/websocket/connection-service.d.ts +0 -1
  98. package/dist/types/services/core/websocket/types.d.ts +1 -1
  99. package/dist/types/services/index.d.ts +1 -1
  100. package/dist/types/services/task/Task.d.ts +146 -0
  101. package/dist/types/services/task/TaskFactory.d.ts +12 -0
  102. package/dist/types/services/task/TaskUtils.d.ts +39 -8
  103. package/dist/types/services/task/constants.d.ts +5 -4
  104. package/dist/types/services/task/dialer.d.ts +0 -15
  105. package/dist/types/services/task/digital/Digital.d.ts +22 -0
  106. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +906 -0
  107. package/dist/types/services/task/state-machine/actions.d.ts +8 -0
  108. package/dist/types/services/task/state-machine/constants.d.ts +91 -0
  109. package/dist/types/services/task/state-machine/guards.d.ts +78 -0
  110. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  111. package/dist/types/services/task/state-machine/types.d.ts +256 -0
  112. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  113. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  114. package/dist/types/services/task/types.d.ts +539 -88
  115. package/dist/types/services/task/voice/Voice.d.ts +183 -0
  116. package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
  117. package/dist/types/types.d.ts +68 -0
  118. package/dist/types/webex.d.ts +1 -0
  119. package/dist/types.js +70 -0
  120. package/dist/types.js.map +1 -1
  121. package/dist/webex.js +14 -2
  122. package/dist/webex.js.map +1 -1
  123. package/package.json +14 -11
  124. package/src/cc.ts +91 -177
  125. package/src/constants.ts +13 -2
  126. package/src/index.ts +14 -5
  127. package/src/metrics/ai-docs/AGENTS.md +348 -0
  128. package/src/metrics/ai-docs/ARCHITECTURE.md +336 -0
  129. package/src/metrics/behavioral-events.ts +28 -14
  130. package/src/metrics/constants.ts +7 -8
  131. package/src/services/ApiAiAssistant.ts +2 -4
  132. package/src/services/agent/ai-docs/AGENTS.md +238 -0
  133. package/src/services/agent/ai-docs/ARCHITECTURE.md +302 -0
  134. package/src/services/ai-docs/AGENTS.md +384 -0
  135. package/src/services/config/Util.ts +2 -3
  136. package/src/services/config/ai-docs/AGENTS.md +253 -0
  137. package/src/services/config/ai-docs/ARCHITECTURE.md +424 -0
  138. package/src/services/config/types.ts +108 -20
  139. package/src/services/constants.ts +0 -1
  140. package/src/services/core/Err.ts +0 -1
  141. package/src/services/core/Utils.ts +90 -67
  142. package/src/services/core/ai-docs/AGENTS.md +379 -0
  143. package/src/services/core/ai-docs/ARCHITECTURE.md +696 -0
  144. package/src/services/core/aqm-reqs.ts +22 -100
  145. package/src/services/core/websocket/WebSocketManager.ts +4 -23
  146. package/src/services/core/websocket/types.ts +1 -1
  147. package/src/services/index.ts +1 -2
  148. package/src/services/task/Task.ts +785 -0
  149. package/src/services/task/TaskFactory.ts +55 -0
  150. package/src/services/task/TaskManager.ts +579 -633
  151. package/src/services/task/TaskUtils.ts +175 -31
  152. package/src/services/task/ai-docs/AGENTS.md +448 -0
  153. package/src/services/task/ai-docs/ARCHITECTURE.md +573 -0
  154. package/src/services/task/constants.ts +5 -4
  155. package/src/services/task/dialer.ts +1 -56
  156. package/src/services/task/digital/Digital.ts +95 -0
  157. package/src/services/task/state-machine/TaskStateMachine.ts +793 -0
  158. package/src/services/task/state-machine/actions.ts +422 -0
  159. package/src/services/task/state-machine/ai-docs/AGENTS.md +495 -0
  160. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1135 -0
  161. package/src/services/task/state-machine/constants.ts +150 -0
  162. package/src/services/task/state-machine/guards.ts +303 -0
  163. package/src/services/task/state-machine/index.ts +28 -0
  164. package/src/services/task/state-machine/types.ts +228 -0
  165. package/src/services/task/state-machine/uiControlsComputer.ts +542 -0
  166. package/src/services/task/taskDataNormalizer.ts +137 -0
  167. package/src/services/task/types.ts +641 -95
  168. package/src/services/task/voice/Voice.ts +1255 -0
  169. package/src/services/task/voice/WebRTC.ts +187 -0
  170. package/src/types.ts +88 -5
  171. package/src/utils/AGENTS.md +276 -0
  172. package/src/webex.js +2 -0
  173. package/test/unit/spec/cc.ts +59 -142
  174. package/test/unit/spec/logger-proxy.ts +70 -0
  175. package/test/unit/spec/services/ApiAiAssistant.ts +17 -0
  176. package/test/unit/spec/services/config/index.ts +26 -55
  177. package/test/unit/spec/services/core/Utils.ts +103 -52
  178. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +48 -112
  179. package/test/unit/spec/services/core/websocket/connection-service.ts +5 -4
  180. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  181. package/test/unit/spec/services/task/Task.ts +416 -0
  182. package/test/unit/spec/services/task/TaskFactory.ts +62 -0
  183. package/test/unit/spec/services/task/TaskManager.ts +781 -1735
  184. package/test/unit/spec/services/task/TaskUtils.ts +125 -0
  185. package/test/unit/spec/services/task/dialer.ts +112 -198
  186. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  187. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +473 -0
  188. package/test/unit/spec/services/task/state-machine/guards.ts +288 -0
  189. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  190. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +147 -0
  191. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  192. package/test/unit/spec/services/task/voice/Voice.ts +587 -0
  193. package/test/unit/spec/services/task/voice/WebRTC.ts +242 -0
  194. package/umd/contact-center.min.js +2 -2
  195. package/umd/contact-center.min.js.map +1 -1
  196. package/dist/services/task/index.js +0 -1525
  197. package/dist/services/task/index.js.map +0 -1
  198. package/dist/types/services/task/index.d.ts +0 -650
  199. package/src/services/task/index.ts +0 -1801
  200. package/test/unit/spec/services/task/index.ts +0 -2184
@@ -0,0 +1,793 @@
1
+ /**
2
+ * Task State Machine Configuration
3
+ *
4
+ * This file defines the XState state machine configuration for contact center tasks.
5
+ * It orchestrates state transitions, guards, and actions for task lifecycle management.
6
+ *
7
+ * GUARD FUNCTIONS: All guard logic is centralized in guards.ts for reusability and testing.
8
+ * This file imports and uses those guards via the `guards` object.
9
+ */
10
+
11
+ import {setup} from 'xstate';
12
+ import {TaskContext, TaskEventPayload, UIControlConfig, TaskActionsMap} from './types';
13
+ import {TaskState, TaskEvent} from './constants';
14
+ import {actions, createInitialContext} from './actions';
15
+ import {guards} from './guards';
16
+ import {getIsCustomerInCall} from '../TaskUtils';
17
+
18
+ type TaskActionConfigMap = {[K in keyof typeof actions]: undefined};
19
+
20
+ const taskStateMachineSetup = setup<
21
+ TaskContext,
22
+ TaskEventPayload,
23
+ Record<string, never>,
24
+ Record<string, never>,
25
+ TaskActionConfigMap
26
+ >({
27
+ actors: {},
28
+ types: {
29
+ context: {} as TaskContext,
30
+ events: {} as TaskEventPayload,
31
+ },
32
+ });
33
+
34
+ /**
35
+ * Get task state machine configuration with UI control config
36
+ * Defines all states, transitions, guards, and actions for task management
37
+ *
38
+ * @param uiControlConfig - UI control configuration
39
+ * @returns State machine configuration object
40
+ */
41
+ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
42
+ /**
43
+ * Event mapping reference (CC WebSocket -> TaskEvent)
44
+ *
45
+ * AgentContactReserved -> TaskEvent.TASK_INCOMING
46
+ * AgentOfferContact -> TaskEvent.TASK_OFFERED
47
+ * AgentOfferConsult -> TaskEvent.OFFER_CONSULT
48
+ * AgentConsulting -> TaskEvent.CONSULTING_ACTIVE
49
+ * AgentConsultCreated -> TaskEvent.CONSULT_CREATED
50
+ * AgentConsultTransferred -> TaskEvent.TRANSFER_SUCCESS
51
+ * AgentContactAssigned -> TaskEvent.ASSIGN
52
+ * AgentContactHeld -> TaskEvent.HOLD_SUCCESS
53
+ * AgentContactUnheld -> TaskEvent.UNHOLD_SUCCESS
54
+ * AgentConsultEnded -> TaskEvent.CONSULT_END
55
+ * AgentContactEnded -> TaskEvent.CONTACT_ENDED
56
+ * AgentWrapup -> TaskEvent.TASK_WRAPUP (wrapUpRequired)
57
+ * AgentWrappedup -> TaskEvent.WRAPUP_COMPLETE
58
+ *
59
+ * (See TaskManager.mapEventToTaskStateMachineEvent for the full mapping table.)
60
+ */
61
+ return {
62
+ id: 'taskStateMachine',
63
+ initial: TaskState.IDLE,
64
+ context: createInitialContext(uiControlConfig, TaskState.IDLE),
65
+ on: {
66
+ [TaskEvent.RECORDING_STARTED]: {
67
+ actions: ['updateTaskData', 'emitTaskRecordingStarted'],
68
+ },
69
+ [TaskEvent.CONTACT_UPDATED]: {
70
+ actions: ['updateTaskData', 'syncTaskDataFromEvent'],
71
+ },
72
+ [TaskEvent.CONTACT_OWNER_CHANGED]: {
73
+ actions: ['updateTaskData', 'syncTaskDataFromEvent'],
74
+ },
75
+ // HYDRATE: Update task data from AgentContact event
76
+ // Note: State restoration with transitions is handled in IDLE state.
77
+ // This root-level handler is for when task is already in another state (just updates data).
78
+ [TaskEvent.HYDRATE]: {
79
+ actions: ['updateTaskData', 'emitTaskHydrate'],
80
+ },
81
+ },
82
+ states: {
83
+ [TaskState.IDLE]: {
84
+ on: {
85
+ // HYDRATE: Restore state machine to correct state based on hydrated task data
86
+ // This handles page refresh/reconnection scenarios where task needs to be restored
87
+ // IMPORTANT: This MUST be in IDLE state (not root) because root-level events cannot
88
+ // transition to child states in XState
89
+ [TaskEvent.HYDRATE]: [
90
+ {
91
+ guard: guards.isInteractionTerminated,
92
+ target: TaskState.WRAPPING_UP,
93
+ actions: ['updateTaskData', 'markEnded', 'emitTaskHydrate'],
94
+ },
95
+ {
96
+ guard: guards.isInteractionConsulting,
97
+ target: TaskState.CONSULTING,
98
+ actions: ['updateTaskData', 'emitTaskHydrate'],
99
+ },
100
+ {
101
+ guard: guards.isInteractionHeld,
102
+ target: TaskState.HELD,
103
+ actions: ['updateTaskData', 'emitTaskHydrate'],
104
+ },
105
+ {
106
+ guard: guards.isInteractionConnected,
107
+ target: TaskState.CONNECTED,
108
+ actions: ['updateTaskData', 'emitTaskHydrate'],
109
+ },
110
+ {
111
+ guard: guards.isConferencingByParticipants,
112
+ target: TaskState.CONFERENCING,
113
+ actions: ['updateTaskData', 'emitTaskHydrate'],
114
+ },
115
+ {
116
+ // Default: just update data, stay in IDLE
117
+ actions: ['updateTaskData', 'emitTaskHydrate'],
118
+ },
119
+ ],
120
+ // AgentContactReserved (applicable for direct incoming/consult/transfer/outdial)
121
+ [TaskEvent.TASK_INCOMING]: {
122
+ target: TaskState.OFFERED,
123
+ actions: ['initializeTask', 'emitTaskIncoming'],
124
+ },
125
+
126
+ // EP-DN split-leg ordering can deliver AgentConsulting before HYDRATE/TASK_INCOMING.
127
+ // Do not drop it in IDLE; bootstrap to CONSULTING using event taskData.
128
+ [TaskEvent.CONSULTING_ACTIVE]: {
129
+ target: TaskState.CONSULTING,
130
+ actions: [
131
+ 'updateTaskData',
132
+ 'setConsultInitiator',
133
+ 'setConsultDestination',
134
+ 'setConsultFromConference',
135
+ 'setConsultAgentJoined',
136
+ 'emitTaskConsultAccepted',
137
+ 'emitTaskConsulting',
138
+ ],
139
+ },
140
+ },
141
+ },
142
+
143
+ [TaskState.OFFERED]: {
144
+ on: {
145
+ // AgentContactOffer
146
+ [TaskEvent.TASK_OFFERED]: {
147
+ actions: ['updateTaskData', 'emitTaskOfferContact', 'requestAutoAnswer'],
148
+ },
149
+ // AgentContactAssigned
150
+ [TaskEvent.ASSIGN]: [
151
+ {
152
+ guard: guards.isConsultingAssignment,
153
+ target: TaskState.CONSULTING,
154
+ actions: ['updateTaskData', 'emitTaskConsultAccepted', 'emitTaskConsulting'],
155
+ },
156
+ {
157
+ target: TaskState.CONNECTED,
158
+ actions: ['updateTaskData', 'emitTaskAssigned'],
159
+ },
160
+ ],
161
+ // AgentOfferContactRONA
162
+ [TaskEvent.RONA]: {
163
+ target: TaskState.TERMINATED,
164
+ actions: ['updateTaskData', 'markEnded', 'emitTaskReject'],
165
+ },
166
+ [TaskEvent.TASK_WRAPUP]: {
167
+ target: TaskState.TERMINATED,
168
+ actions: ['updateTaskData', 'markEnded', 'emitTaskEnd'],
169
+ },
170
+ [TaskEvent.CONTACT_ENDED]: {
171
+ target: TaskState.TERMINATED,
172
+ actions: ['updateTaskData', 'markEnded', 'emitTaskEnd'],
173
+ },
174
+ // This needs to be handled for all assign failed scenarios (contact, buddy)
175
+ // [AgentContactAssignFailed, AgentCtqFailed, AgentBlindTransferFailed,
176
+ // AgentVTeamTransferFailed, AgentConsultTransferFailed]
177
+ [TaskEvent.ASSIGN_FAILED]: {
178
+ target: TaskState.TERMINATED,
179
+ actions: ['updateTaskData', 'markEnded', 'emitTaskReject'],
180
+ },
181
+ [TaskEvent.INVITE_FAILED]: {
182
+ target: TaskState.TERMINATED,
183
+ actions: ['updateTaskData', 'markEnded', 'emitTaskReject'],
184
+ },
185
+ [TaskEvent.OUTBOUND_FAILED]: [
186
+ {
187
+ guard: guards.shouldWrapUp,
188
+ target: TaskState.WRAPPING_UP,
189
+ actions: ['updateTaskData', 'markEnded', 'emitTaskOutdialFailed', 'emitTaskWrapup'],
190
+ },
191
+ {
192
+ target: TaskState.TERMINATED,
193
+ actions: ['updateTaskData', 'markEnded', 'emitTaskOutdialFailed', 'emitTaskReject'],
194
+ },
195
+ ],
196
+ // AgentConsulting comes for received after the initial consult is accepted
197
+ [TaskEvent.CONSULTING_ACTIVE]: [
198
+ {
199
+ target: TaskState.CONSULTING,
200
+ actions: [
201
+ 'updateTaskData',
202
+ 'setConsultAgentJoined',
203
+ 'emitTaskConsultAccepted',
204
+ 'emitTaskConsulting',
205
+ ],
206
+ },
207
+ ],
208
+ // agentOfferConsult happens only on the receiver side of consult
209
+ [TaskEvent.OFFER_CONSULT]: {
210
+ actions: ['updateTaskData', 'emitTaskOfferConsult', 'requestAutoAnswer'],
211
+ },
212
+ // AgentConsultFailed - when consulted agent (Agent 2) doesn't answer (RONA or decline)
213
+ // Clears the incoming consult notification by transitioning to TERMINATED
214
+ [TaskEvent.CONSULT_FAILED]: {
215
+ target: TaskState.TERMINATED,
216
+ actions: ['updateTaskData', 'clearConsultState', 'emitTaskReject'],
217
+ },
218
+ // AgentConsultEnded - when consult initiator (Agent 1) ends the consult before
219
+ // the consulted agent (Agent 2) accepts. Clears the incoming notification.
220
+ [TaskEvent.CONSULT_END]: {
221
+ target: TaskState.TERMINATED,
222
+ actions: ['updateTaskData', 'clearConsultState', 'emitTaskReject'],
223
+ },
224
+ },
225
+ },
226
+
227
+ [TaskState.CONNECTED]: {
228
+ on: {
229
+ // AgentConsulting may arrive while machine is CONNECTED (EP-DN/event ordering).
230
+ // Derive consultInitiator from payload so controls are set correctly.
231
+ [TaskEvent.CONSULTING_ACTIVE]: {
232
+ target: TaskState.CONSULTING,
233
+ actions: [
234
+ 'updateTaskData',
235
+ 'setConsultInitiator',
236
+ 'setConsultDestination',
237
+ 'setConsultAgentJoined',
238
+ 'emitTaskConsultAccepted',
239
+ 'emitTaskConsulting',
240
+ ],
241
+ },
242
+ // AgentContactAssigned can be resent after consult transfers; keep context in sync
243
+ [TaskEvent.ASSIGN]: {
244
+ target: TaskState.CONNECTED,
245
+ actions: ['updateTaskData', 'emitTaskAssigned'],
246
+ },
247
+ // Click of hold button
248
+ [TaskEvent.HOLD_INITIATED]: {
249
+ target: TaskState.HOLD_INITIATING,
250
+ },
251
+ // Remote hold from another login session (multi-login)
252
+ [TaskEvent.HOLD_SUCCESS]: {
253
+ target: TaskState.HELD,
254
+ actions: ['updateTaskData', 'setHoldState', 'emitTaskHold'],
255
+ },
256
+ // Click of the consult button
257
+ [TaskEvent.CONSULT]: {
258
+ target: TaskState.CONSULT_INITIATING,
259
+ actions: ['setConsultInitiator', 'setConsultDestination'],
260
+ },
261
+ // AgentConsultTransferred / AgentVTeamTransferred / AgentBlindTransferred
262
+ [TaskEvent.TRANSFER_SUCCESS]: [
263
+ {
264
+ guard: guards.shouldWrapUpOrIsInitiator,
265
+ target: TaskState.WRAPPING_UP,
266
+ actions: ['updateTaskData', 'markEnded', 'clearConsultState', 'emitTaskWrapup'],
267
+ },
268
+ {
269
+ // Receiver goes to connected as he receives transferSuccess event
270
+ actions: ['updateTaskData', 'clearConsultState'],
271
+ },
272
+ ],
273
+ [TaskEvent.TRANSFER_FAILED]: {
274
+ actions: ['updateTaskData'],
275
+ },
276
+ // AgentContactEnded Event
277
+ [TaskEvent.CONTACT_ENDED]: [
278
+ {
279
+ // Conference still active → CONFERENCING
280
+ guard: guards.conferenceInProgressFromEvent,
281
+ target: TaskState.CONFERENCING,
282
+ actions: ['updateTaskData', 'emitTaskConferenceStarted', 'requestCleanup'],
283
+ },
284
+ {
285
+ // Agent should wrap up → WRAPPING_UP
286
+ guard: guards.shouldWrapUp,
287
+ target: TaskState.WRAPPING_UP,
288
+ actions: ['updateTaskData', 'markEnded', 'emitTaskWrapup', 'requestCleanup'],
289
+ },
290
+ {
291
+ // Consulted agent → TERMINATED
292
+ target: TaskState.TERMINATED,
293
+ actions: ['updateTaskData', 'markEnded', 'emitTaskEnd'],
294
+ },
295
+ ],
296
+ [TaskEvent.TASK_WRAPUP]: {
297
+ target: TaskState.WRAPPING_UP,
298
+ actions: ['updateTaskData', 'markEnded', 'emitTaskWrapup'],
299
+ },
300
+ [TaskEvent.PAUSE_RECORDING]: {
301
+ actions: ['updateTaskData', 'setRecordingState', 'emitTaskRecordingPaused'],
302
+ },
303
+ [TaskEvent.RESUME_RECORDING]: {
304
+ actions: ['updateTaskData', 'setRecordingState', 'emitTaskRecordingResumed'],
305
+ },
306
+ },
307
+ },
308
+
309
+ [TaskState.HOLD_INITIATING]: {
310
+ on: {
311
+ // AgentContactHeld Event
312
+ [TaskEvent.HOLD_SUCCESS]: {
313
+ target: TaskState.HELD,
314
+ actions: ['updateTaskData', 'setHoldState', 'emitTaskHold'],
315
+ },
316
+ // AgentContactHoldFailed Event
317
+ [TaskEvent.HOLD_FAILED]: {
318
+ target: TaskState.CONNECTED,
319
+ actions: ['updateTaskData'],
320
+ },
321
+ },
322
+ },
323
+
324
+ [TaskState.HELD]: {
325
+ on: {
326
+ // Click of the unhold button
327
+ [TaskEvent.UNHOLD_INITIATED]: {
328
+ target: TaskState.RESUME_INITIATING,
329
+ },
330
+ // Remote resume from another login session (multi-login)
331
+ [TaskEvent.UNHOLD_SUCCESS]: {
332
+ target: TaskState.CONNECTED,
333
+ actions: ['updateTaskData', 'setHoldState', 'emitTaskResume'],
334
+ },
335
+ // Click of the consult button
336
+ [TaskEvent.CONSULT]: {
337
+ target: TaskState.CONSULT_INITIATING,
338
+ actions: ['setConsultInitiator', 'setConsultDestination'],
339
+ },
340
+ // TODO: This may not be a valid transition, need to be removed
341
+ // AgentConsultTransferred / AgentVTeamTransferred / AgentBlindTransferred
342
+ [TaskEvent.TRANSFER_SUCCESS]: [
343
+ {
344
+ guard: guards.shouldWrapUpOrIsInitiator,
345
+ target: TaskState.WRAPPING_UP,
346
+ actions: ['updateTaskData', 'markEnded', 'emitTaskWrapup'],
347
+ },
348
+ {
349
+ target: TaskState.CONNECTED,
350
+ actions: ['updateTaskData', 'clearConsultState'],
351
+ },
352
+ ],
353
+ [TaskEvent.TRANSFER_FAILED]: {
354
+ actions: ['updateTaskData'],
355
+ },
356
+ [TaskEvent.CONTACT_ENDED]: [
357
+ {
358
+ guard: guards.conferenceInProgressFromEvent,
359
+ target: TaskState.CONFERENCING,
360
+ actions: ['updateTaskData', 'emitTaskConferenceStarted', 'requestCleanup'],
361
+ },
362
+ {
363
+ guard: guards.shouldWrapUp,
364
+ target: TaskState.WRAPPING_UP,
365
+ actions: ['updateTaskData', 'markEnded', 'emitTaskWrapup', 'requestCleanup'],
366
+ },
367
+ {
368
+ target: TaskState.TERMINATED,
369
+ actions: ['updateTaskData', 'markEnded', 'emitTaskEnd'],
370
+ },
371
+ ],
372
+ // TODO: This may not be a valid transition, this needs to be checked as well
373
+ [TaskEvent.TASK_WRAPUP]: {
374
+ target: TaskState.WRAPPING_UP,
375
+ actions: ['updateTaskData', 'markEnded', 'emitTaskWrapup'],
376
+ },
377
+ },
378
+ },
379
+
380
+ [TaskState.RESUME_INITIATING]: {
381
+ on: {
382
+ [TaskEvent.UNHOLD_SUCCESS]: {
383
+ target: TaskState.CONNECTED,
384
+ actions: ['updateTaskData', 'setHoldState', 'emitTaskResume'],
385
+ },
386
+ [TaskEvent.UNHOLD_FAILED]: {
387
+ target: TaskState.HELD,
388
+ },
389
+ },
390
+ },
391
+
392
+ [TaskState.CONSULT_INITIATING]: {
393
+ on: {
394
+ [TaskEvent.HOLD_SUCCESS]: {
395
+ actions: ['updateTaskData'],
396
+ },
397
+ [TaskEvent.HOLD_FAILED]: {
398
+ target: TaskState.CONNECTED,
399
+ actions: ['updateTaskData', 'handleConsultFailed'],
400
+ },
401
+ // AgentConsulting
402
+ // NOTE: Don't set consultDestinationAgentJoined here - wait for CONSULTING_ACTIVE
403
+ [TaskEvent.CONSULT_SUCCESS]: {
404
+ target: TaskState.CONSULTING,
405
+ actions: ['updateTaskData', 'setConsultInitiator'],
406
+ },
407
+ // AgentConsultFailed, API Failures, AgentCtqFailed
408
+ [TaskEvent.CONSULT_FAILED]: [
409
+ {
410
+ // Consult from conference → back to CONFERENCING
411
+ guard: ({context}) => context.consultFromConference === true,
412
+ target: TaskState.CONFERENCING,
413
+ actions: ['updateTaskData', 'handleConsultFailed'],
414
+ },
415
+ {
416
+ guard: guards.isPrimaryMediaOnHold,
417
+ target: TaskState.HELD,
418
+ actions: ['updateTaskData', 'handleConsultFailed'],
419
+ },
420
+ {
421
+ target: TaskState.CONNECTED,
422
+ actions: ['updateTaskData', 'handleConsultFailed'],
423
+ },
424
+ ],
425
+ // AgentCtqCancelled Event
426
+ [TaskEvent.CTQ_CANCEL]: [
427
+ {
428
+ guard: guards.isPrimaryMediaOnHold,
429
+ target: TaskState.HELD,
430
+ actions: ['updateTaskData', 'clearConsultState'],
431
+ },
432
+ {
433
+ target: TaskState.CONNECTED,
434
+ actions: ['updateTaskData', 'clearConsultState'],
435
+ },
436
+ ],
437
+ },
438
+ },
439
+
440
+ [TaskState.CONSULTING]: {
441
+ on: {
442
+ // AgentConsulting updates consulted agent arrival
443
+ [TaskEvent.CONSULTING_ACTIVE]: {
444
+ actions: [
445
+ 'updateTaskData',
446
+ 'setConsultAgentJoined',
447
+ 'setConsultDestination',
448
+ 'emitTaskConsulting',
449
+ ],
450
+ },
451
+
452
+ // AgentConsultEnded
453
+ [TaskEvent.CONSULT_END]: [
454
+ {
455
+ // Initiator returning to conference (flag set OR backend still shows conference)
456
+ guard: ({context, event}) =>
457
+ context.consultInitiator === true &&
458
+ (context.consultFromConference === true ||
459
+ guards.conferenceInProgressFromEvent({context, event})),
460
+ target: TaskState.CONFERENCING,
461
+ actions: ['updateTaskData', 'clearConsultState', 'emitTaskConsultEnd'],
462
+ },
463
+ {
464
+ // Initiator already switched back to the main/customer leg
465
+ guard: ({context}) =>
466
+ context.consultInitiator === true && context.consultCallHeld === true,
467
+ target: TaskState.CONNECTED,
468
+ actions: ['updateTaskData', 'clearConsultState', 'emitTaskConsultEnd'],
469
+ },
470
+ {
471
+ // Initiator (no conference) → HELD
472
+ guard: ({context}) => context.consultInitiator === true,
473
+ target: TaskState.HELD,
474
+ actions: ['updateTaskData', 'clearConsultState', 'emitTaskConsultEnd'],
475
+ },
476
+ {
477
+ // Consulted agent → TERMINATED
478
+ target: TaskState.TERMINATED,
479
+ actions: ['updateTaskData'],
480
+ },
481
+ ],
482
+
483
+ // Switch between consult and main call (UI-driven toggle)
484
+ [TaskEvent.SWITCH_TO_MAIN_CALL]: {
485
+ actions: ['handleSwitchToMainCall', 'emitTaskSwitchCall'],
486
+ },
487
+ [TaskEvent.SWITCH_TO_CONSULT]: {
488
+ actions: ['handleSwitchToConsult', 'emitTaskSwitchCall'],
489
+ },
490
+ [TaskEvent.HOLD_SUCCESS]: {
491
+ actions: ['updateTaskData', 'setHoldState', 'emitTaskHold'],
492
+ },
493
+ [TaskEvent.UNHOLD_SUCCESS]: {
494
+ actions: ['updateTaskData', 'setHoldState', 'emitTaskResume'],
495
+ },
496
+
497
+ [TaskEvent.TRANSFER_SUCCESS]: [
498
+ {
499
+ guard: guards.shouldWrapUpOrIsInitiator,
500
+ target: TaskState.WRAPPING_UP,
501
+ actions: ['updateTaskData', 'markEnded', 'clearConsultState', 'emitTaskWrapup'],
502
+ },
503
+ {
504
+ target: TaskState.CONNECTED,
505
+ actions: ['updateTaskData', 'clearConsultState'],
506
+ },
507
+ ],
508
+ [TaskEvent.TRANSFER_FAILED]: {
509
+ actions: ['updateTaskData'],
510
+ },
511
+ [TaskEvent.TRANSFER_CONFERENCE]: {
512
+ // Track that this agent initiated the conference transfer so we can
513
+ // apply the correct lifecycle transition when success arrives.
514
+ actions: ['setTransferConferenceRequested', 'emitTaskTransferConference'],
515
+ },
516
+ [TaskEvent.TRANSFER_CONFERENCE_SUCCESS]: [
517
+ {
518
+ guard: ({context}) => context.transferConferenceRequested !== true,
519
+ actions: [
520
+ 'updateTaskData',
521
+ 'handleTransferConferenceSuccess',
522
+ 'clearTransferConferenceRequested',
523
+ ],
524
+ },
525
+ {
526
+ guard: guards.shouldWrapUp,
527
+ target: TaskState.WRAPPING_UP,
528
+ actions: [
529
+ 'updateTaskData',
530
+ 'markEnded',
531
+ 'clearConsultState',
532
+ 'handleTransferConferenceSuccess',
533
+ 'clearTransferConferenceRequested',
534
+ 'emitTaskWrapup',
535
+ ],
536
+ },
537
+ {
538
+ // Non-initiator (consulted agent) stays in CONFERENCING
539
+ guard: ({context}) => !context.consultInitiator,
540
+ target: TaskState.CONFERENCING,
541
+ actions: [
542
+ 'updateTaskData',
543
+ 'clearConsultState',
544
+ 'handleTransferConferenceSuccess',
545
+ 'clearTransferConferenceRequested',
546
+ ],
547
+ },
548
+ {
549
+ target: TaskState.TERMINATED,
550
+ actions: [
551
+ 'updateTaskData',
552
+ 'markEnded',
553
+ 'clearConsultState',
554
+ 'handleTransferConferenceSuccess',
555
+ 'clearTransferConferenceRequested',
556
+ 'emitTaskEnd',
557
+ ],
558
+ },
559
+ ],
560
+ [TaskEvent.TRANSFER_CONFERENCE_FAILED]: {
561
+ actions: ['clearTransferConferenceRequested', 'emitTaskTransferConferenceFailed'],
562
+ },
563
+
564
+ // AgentContactAssigned - receiver side becomes connected to customer
565
+ [TaskEvent.ASSIGN]: {
566
+ target: TaskState.CONNECTED,
567
+ actions: ['updateTaskData', 'emitTaskAssigned'],
568
+ },
569
+ // AgentContactEnded
570
+ [TaskEvent.CONTACT_ENDED]: {
571
+ target: TaskState.WRAPPING_UP,
572
+ actions: [
573
+ 'updateTaskData',
574
+ 'markEnded',
575
+ 'clearConsultState',
576
+ 'emitTaskWrapup',
577
+ 'requestCleanup',
578
+ ],
579
+ },
580
+ [TaskEvent.TASK_WRAPUP]: {
581
+ target: TaskState.WRAPPING_UP,
582
+ actions: ['updateTaskData', 'markEnded', 'clearConsultState', 'emitTaskWrapup'],
583
+ },
584
+ [TaskEvent.MERGE_TO_CONFERENCE]: {
585
+ target: TaskState.CONF_INITIATING,
586
+ },
587
+ // AgentConsultConferenced, ParticipantJoinedConference
588
+ [TaskEvent.CONFERENCE_START]: {
589
+ target: TaskState.CONFERENCING,
590
+ actions: ['handleConferenceStarted', 'clearConsultState'],
591
+ },
592
+ },
593
+ },
594
+
595
+ [TaskState.CONF_INITIATING]: {
596
+ on: {
597
+ // AgentConsultConferenced, ParticipantJoinedConference
598
+ [TaskEvent.CONFERENCE_START]: {
599
+ target: TaskState.CONFERENCING,
600
+ actions: ['handleConferenceStarted'],
601
+ },
602
+ // AgentConsultConferenceFailed
603
+ [TaskEvent.CONFERENCE_FAILED]: {
604
+ target: TaskState.CONSULTING,
605
+ actions: ['handleConferenceFailed', 'emitTaskConferenceFailed'],
606
+ },
607
+ },
608
+ },
609
+
610
+ [TaskState.CONFERENCING]: {
611
+ on: {
612
+ [TaskEvent.CONFERENCE_START]: {
613
+ actions: ['updateTaskData', 'clearConsultState', 'emitTaskConferenceStarted'],
614
+ },
615
+ [TaskEvent.EXIT_CONFERENCE_SUCCESS]: [
616
+ {
617
+ guard: guards.shouldWrapUp,
618
+ target: TaskState.WRAPPING_UP,
619
+ actions: ['updateTaskData', 'markEnded', 'clearConsultState', 'emitTaskWrapup'],
620
+ },
621
+ {
622
+ target: TaskState.TERMINATED,
623
+ actions: ['updateTaskData', 'markEnded', 'clearConsultState', 'emitTaskEnd'],
624
+ },
625
+ ],
626
+
627
+ // Needed as all agents in conference get this event, hence we need to clear the consult state
628
+ [TaskEvent.CONSULT_END]: {
629
+ actions: ['updateTaskData', 'clearConsultState'],
630
+ },
631
+
632
+ [TaskEvent.HOLD_SUCCESS]: {
633
+ actions: ['updateTaskData', 'setHoldState', 'emitTaskHold'],
634
+ },
635
+ [TaskEvent.UNHOLD_SUCCESS]: {
636
+ actions: ['updateTaskData', 'setHoldState', 'emitTaskResume'],
637
+ },
638
+
639
+ // Start a new consult from within an active conference
640
+ [TaskEvent.CONSULT]: {
641
+ target: TaskState.CONSULT_INITIATING,
642
+ actions: ['setConsultInitiator', 'setConsultDestination', 'setConsultFromConference'],
643
+ },
644
+
645
+ // Participant leaves - handle conference downgrade scenarios
646
+ [TaskEvent.PARTICIPANT_LEAVE]: [
647
+ {
648
+ // Only the leaving agent should wrap up → WRAPPING_UP
649
+ guard: (params) =>
650
+ guards.didCurrentAgentLeaveConference(params) && guards.shouldWrapUp(params),
651
+ target: TaskState.WRAPPING_UP,
652
+ actions: [
653
+ 'updateTaskData',
654
+ 'handleParticipantLeft',
655
+ 'markEnded',
656
+ 'clearConsultState',
657
+ 'emitTaskParticipantLeft',
658
+ 'emitTaskWrapup',
659
+ ],
660
+ },
661
+ {
662
+ // Only the leaving agent (no wrapup) → TERMINATED
663
+ guard: guards.didCurrentAgentLeaveConference,
664
+ target: TaskState.TERMINATED,
665
+ actions: [
666
+ 'updateTaskData',
667
+ 'handleParticipantLeft',
668
+ 'markEnded',
669
+ 'clearConsultState',
670
+ 'emitTaskParticipantLeft',
671
+ 'emitTaskEnd',
672
+ ],
673
+ },
674
+ {
675
+ // Conference downgraded, customer present → CONNECTED
676
+ guard: (params) =>
677
+ !guards.didCurrentAgentLeaveConference(params) &&
678
+ guards.shouldDowngradeConferenceToConnected(params),
679
+ target: TaskState.CONNECTED,
680
+ actions: [
681
+ 'updateTaskData',
682
+ 'handleParticipantLeft',
683
+ 'clearConsultState',
684
+ 'emitTaskParticipantLeft',
685
+ 'emitTaskConferenceEnded',
686
+ ],
687
+ },
688
+ {actions: ['updateTaskData', 'handleParticipantLeft', 'emitTaskParticipantLeft']},
689
+ ],
690
+
691
+ [TaskEvent.TRANSFER_CONFERENCE]: {
692
+ actions: ['setTransferConferenceRequested', 'emitTaskTransferConference'],
693
+ },
694
+ [TaskEvent.TRANSFER_CONFERENCE_SUCCESS]: [
695
+ {
696
+ // Not initiated by this agent → just refresh backend state.
697
+ guard: ({context}) => context.transferConferenceRequested !== true,
698
+ actions: [
699
+ 'updateTaskData',
700
+ 'handleTransferConferenceSuccess',
701
+ 'clearTransferConferenceRequested',
702
+ ],
703
+ },
704
+ ],
705
+ [TaskEvent.TRANSFER_CONFERENCE_FAILED]: {
706
+ actions: ['clearTransferConferenceRequested', 'emitTaskTransferConferenceFailed'],
707
+ },
708
+
709
+ // Conference ends explicitly
710
+ [TaskEvent.CONFERENCE_END]: [
711
+ {
712
+ // Agent who should wrap up → WRAPPING_UP
713
+ guard: guards.shouldWrapUp,
714
+ target: TaskState.WRAPPING_UP,
715
+ actions: ['updateTaskData', 'markEnded', 'clearConsultState', 'emitTaskWrapup'],
716
+ },
717
+ {
718
+ // Customer still in call → CONNECTED
719
+ guard: ({context, event}) => {
720
+ if (context.exitingConference === true) return false;
721
+ const taskData = (event as any)?.taskData ?? context.taskData;
722
+ if (!taskData?.interaction) return false;
723
+ const mainCallId = taskData.interaction.mainInteractionId || taskData.interactionId;
724
+ if (!mainCallId) return false;
725
+
726
+ return getIsCustomerInCall(taskData.interaction, mainCallId);
727
+ },
728
+ target: TaskState.CONNECTED,
729
+ actions: ['updateTaskData', 'clearConsultState', 'emitTaskConferenceEnded'],
730
+ },
731
+ {
732
+ // Otherwise → TERMINATED
733
+ target: TaskState.TERMINATED,
734
+ actions: ['updateTaskData', 'markEnded', 'clearConsultState', 'emitTaskEnd'],
735
+ },
736
+ ],
737
+
738
+ // CONTACT_ENDED in conference
739
+ [TaskEvent.CONTACT_ENDED]: [
740
+ {
741
+ // Conference still active → stay
742
+ actions: ['updateTaskData', 'requestCleanup'],
743
+ },
744
+ ],
745
+ },
746
+ },
747
+
748
+ [TaskState.WRAPPING_UP]: {
749
+ // Only emit wrapup event on entry - task:end should only be emitted when COMPLETED
750
+ entry: ['emitTaskWrapup'],
751
+ on: {
752
+ // AgentWrappedup Event
753
+ [TaskEvent.WRAPUP_COMPLETE]: {
754
+ target: TaskState.COMPLETED,
755
+ actions: ['updateTaskData'],
756
+ },
757
+ },
758
+ },
759
+
760
+ [TaskState.COMPLETED]: {
761
+ type: 'final' as const,
762
+ entry: ['emitTaskWrappedup', 'cleanupResources'],
763
+ },
764
+
765
+ [TaskState.TERMINATED]: {
766
+ type: 'final' as const,
767
+ entry: ['cleanupResources'],
768
+ },
769
+ },
770
+ };
771
+ }
772
+
773
+ /**
774
+ * Create a task state machine instance using only the built-in actions.
775
+ * The resulting machine is ready for most consumers that rely on the default
776
+ * context mutators declared in actions.ts.
777
+ *
778
+ * @param uiControlConfig - UI control configuration
779
+ * @returns StateMachine instance for task management
780
+ */
781
+ export function createTaskStateMachine(
782
+ uiControlConfig: UIControlConfig,
783
+ options?: {actions?: Partial<TaskActionsMap>}
784
+ ) {
785
+ return taskStateMachineSetup.createMachine(getTaskStateMachineConfig(uiControlConfig)).provide({
786
+ actions: {
787
+ ...actions,
788
+ ...(options?.actions ?? {}),
789
+ },
790
+ });
791
+ }
792
+
793
+ export type TaskStateMachine = ReturnType<typeof createTaskStateMachine>;