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

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 (58) hide show
  1. package/dist/cc.js +3 -4
  2. package/dist/cc.js.map +1 -1
  3. package/dist/constants.js +1 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/metrics/constants.js +2 -0
  6. package/dist/metrics/constants.js.map +1 -1
  7. package/dist/services/ApiAiAssistant.js +74 -3
  8. package/dist/services/ApiAiAssistant.js.map +1 -1
  9. package/dist/services/config/types.js +9 -1
  10. package/dist/services/config/types.js.map +1 -1
  11. package/dist/services/task/Task.js +32 -0
  12. package/dist/services/task/Task.js.map +1 -1
  13. package/dist/services/task/TaskManager.js +7 -2
  14. package/dist/services/task/TaskManager.js.map +1 -1
  15. package/dist/services/task/TaskUtils.js +3 -1
  16. package/dist/services/task/TaskUtils.js.map +1 -1
  17. package/dist/services/task/state-machine/TaskStateMachine.js +76 -0
  18. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  19. package/dist/services/task/state-machine/actions.js +113 -23
  20. package/dist/services/task/state-machine/actions.js.map +1 -1
  21. package/dist/services/task/state-machine/uiControlsComputer.js +99 -21
  22. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  23. package/dist/types/constants.d.ts +1 -0
  24. package/dist/types/metrics/constants.d.ts +2 -0
  25. package/dist/types/services/ApiAiAssistant.d.ts +10 -2
  26. package/dist/types/services/config/types.d.ts +16 -0
  27. package/dist/types/services/task/Task.d.ts +10 -0
  28. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +110 -0
  29. package/dist/types/services/task/state-machine/actions.d.ts +2 -0
  30. package/dist/types/types.d.ts +24 -0
  31. package/dist/types.js +15 -0
  32. package/dist/types.js.map +1 -1
  33. package/dist/webex.js +1 -1
  34. package/package.json +1 -1
  35. package/src/cc.ts +6 -4
  36. package/src/constants.ts +1 -0
  37. package/src/metrics/constants.ts +2 -0
  38. package/src/services/ApiAiAssistant.ts +102 -2
  39. package/src/services/config/types.ts +8 -0
  40. package/src/services/task/Task.ts +34 -0
  41. package/src/services/task/TaskManager.ts +7 -2
  42. package/src/services/task/TaskUtils.ts +5 -3
  43. package/src/services/task/ai-docs/AGENTS.md +7 -0
  44. package/src/services/task/ai-docs/ARCHITECTURE.md +12 -0
  45. package/src/services/task/state-machine/TaskStateMachine.ts +104 -0
  46. package/src/services/task/state-machine/actions.ts +151 -25
  47. package/src/services/task/state-machine/uiControlsComputer.ts +173 -30
  48. package/src/types.ts +25 -0
  49. package/test/unit/spec/cc.ts +2 -0
  50. package/test/unit/spec/services/ApiAiAssistant.ts +105 -17
  51. package/test/unit/spec/services/task/Task.ts +61 -0
  52. package/test/unit/spec/services/task/TaskManager.ts +42 -0
  53. package/test/unit/spec/services/task/TaskUtils.ts +65 -0
  54. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +676 -0
  55. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +597 -0
  56. package/test/unit/spec/services/task/voice/WebRTC.ts +99 -106
  57. package/umd/contact-center.min.js +2 -2
  58. package/umd/contact-center.min.js.map +1 -1
@@ -78,6 +78,15 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
78
78
  [TaskEvent.HYDRATE]: {
79
79
  actions: ['updateTaskData', 'emitTaskHydrate'],
80
80
  },
81
+ // AgentConsultCreated from Stable Prod while already HELD/CONNECTED (external consult).
82
+ // Child states do not handle CONSULT_CREATED; wire here so updateTaskData + setConsultInitiator run.
83
+ [TaskEvent.CONSULT_CREATED]: {
84
+ actions: ['updateTaskData', 'setConsultInitiator'],
85
+ },
86
+ // AgentConsultFailed (RONA) while HELD/CONNECTED without passing through CONSULT_INITIATING.
87
+ [TaskEvent.CONSULT_FAILED]: {
88
+ actions: ['updateTaskData', 'handleConsultFailed'],
89
+ },
81
90
  },
82
91
  states: {
83
92
  [TaskState.IDLE]: {
@@ -290,6 +299,24 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
290
299
  [TaskEvent.TRANSFER_FAILED]: {
291
300
  actions: ['updateTaskData'],
292
301
  },
302
+ // AgentConsultEnded from Stable Prod while on connected leg (external end consult).
303
+ [TaskEvent.CONSULT_END]: [
304
+ {
305
+ guard: ({context, event}) => {
306
+ if (context.consultInitiator !== true) return false;
307
+ const taskData = getTaskDataFromEvent(event);
308
+ const mainId = taskData?.interaction?.mainInteractionId || taskData?.interactionId;
309
+
310
+ return Boolean(mainId && taskData?.interaction?.media?.[mainId]?.isHold === true);
311
+ },
312
+ target: TaskState.HELD,
313
+ actions: ['updateTaskData', 'clearConsultState', 'emitTaskConsultEnd'],
314
+ },
315
+ {
316
+ guard: ({context}) => context.consultInitiator === true,
317
+ actions: ['updateTaskData', 'clearConsultState', 'emitTaskConsultEnd'],
318
+ },
319
+ ],
293
320
  // AgentContactEnded Event
294
321
  [TaskEvent.CONTACT_ENDED]: [
295
322
  {
@@ -370,6 +397,18 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
370
397
  target: TaskState.CONSULT_INITIATING,
371
398
  actions: ['setConsultInitiator', 'setConsultDestination'],
372
399
  },
400
+ // AgentConsulting while main leg is held (Task Refactor / Stable Prod consult accept).
401
+ [TaskEvent.CONSULTING_ACTIVE]: {
402
+ target: TaskState.CONSULTING,
403
+ actions: [
404
+ 'setConsultInitiator',
405
+ 'setConsultDestination',
406
+ 'setConsultAgentJoined',
407
+ 'updateTaskData',
408
+ 'emitTaskConsultAccepted',
409
+ 'emitTaskConsulting',
410
+ ],
411
+ },
373
412
  // TODO: This may not be a valid transition, need to be removed
374
413
  // AgentConsultTransferred / AgentVTeamTransferred / AgentBlindTransferred
375
414
  [TaskEvent.TRANSFER_SUCCESS]: [
@@ -402,6 +441,10 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
402
441
  actions: ['updateTaskData', 'markEnded', 'emitTaskEnd'],
403
442
  },
404
443
  ],
444
+ // AgentConsultEnded from Stable Prod while main leg is held (external end consult).
445
+ [TaskEvent.CONSULT_END]: {
446
+ actions: ['updateTaskData', 'clearConsultState', 'emitTaskConsultEnd'],
447
+ },
405
448
  // TODO: This may not be a valid transition, this needs to be checked as well
406
449
  [TaskEvent.TASK_WRAPUP]: {
407
450
  target: TaskState.WRAPPING_UP,
@@ -437,6 +480,18 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
437
480
  target: TaskState.CONSULTING,
438
481
  actions: ['updateTaskData', 'setConsultInitiator'],
439
482
  },
483
+ // AgentConsulting (Task Refactor: AgentConsulting event, not CONSULT_SUCCESS)
484
+ [TaskEvent.CONSULTING_ACTIVE]: {
485
+ target: TaskState.CONSULTING,
486
+ actions: [
487
+ 'setConsultInitiator',
488
+ 'setConsultDestination',
489
+ 'setConsultAgentJoined',
490
+ 'updateTaskData',
491
+ 'emitTaskConsultAccepted',
492
+ 'emitTaskConsulting',
493
+ ],
494
+ },
440
495
  // AgentConsultFailed, API Failures, AgentCtqFailed
441
496
  [TaskEvent.CONSULT_FAILED]: [
442
497
  {
@@ -467,6 +522,18 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
467
522
  actions: ['updateTaskData', 'clearConsultState'],
468
523
  },
469
524
  ],
525
+ // AgentConsultEnded from Stable Prod during consult initiation (external end consult).
526
+ [TaskEvent.CONSULT_END]: [
527
+ {
528
+ guard: guards.isPrimaryMediaOnHold,
529
+ target: TaskState.HELD,
530
+ actions: ['updateTaskData', 'clearConsultState', 'emitTaskConsultEnd'],
531
+ },
532
+ {
533
+ target: TaskState.CONNECTED,
534
+ actions: ['updateTaskData', 'clearConsultState', 'emitTaskConsultEnd'],
535
+ },
536
+ ],
470
537
  },
471
538
  },
472
539
 
@@ -482,6 +549,29 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
482
549
  ],
483
550
  },
484
551
 
552
+ // AgentConsultFailed (RONA / consultee declined) while the initiator is already in
553
+ // CONSULTING (AgentConsulting arrived during ringing). Mirror CONSULT_INITIATING so the
554
+ // initiator returns to their own leg (HELD when main is on hold, else CONNECTED) instead
555
+ // of staying in CONSULTING. Without this, handleConsultFailed clears consultInitiator but
556
+ // the machine stays in CONSULTING, so the trailing AgentConsultEnded falls through the
557
+ // CONSULT_END "consulted agent" branch to TERMINATED and wrongly clears the task.
558
+ [TaskEvent.CONSULT_FAILED]: [
559
+ {
560
+ guard: ({context}) => context.consultFromConference === true,
561
+ target: TaskState.CONFERENCING,
562
+ actions: ['updateTaskData', 'handleConsultFailed'],
563
+ },
564
+ {
565
+ guard: guards.isPrimaryMediaOnHold,
566
+ target: TaskState.HELD,
567
+ actions: ['updateTaskData', 'handleConsultFailed'],
568
+ },
569
+ {
570
+ target: TaskState.CONNECTED,
571
+ actions: ['updateTaskData', 'handleConsultFailed'],
572
+ },
573
+ ],
574
+
485
575
  // AgentConsultEnded
486
576
  [TaskEvent.CONSULT_END]: [
487
577
  {
@@ -790,6 +880,20 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
790
880
  actions: ['setConsultInitiator', 'setConsultDestination', 'setConsultFromConference'],
791
881
  },
792
882
 
883
+ // AgentConsulting while still in conference (EP-DN/external ordering).
884
+ [TaskEvent.CONSULTING_ACTIVE]: {
885
+ target: TaskState.CONSULTING,
886
+ actions: [
887
+ 'setConsultInitiator',
888
+ 'setConsultDestination',
889
+ 'setConsultFromConference',
890
+ 'setConsultAgentJoined',
891
+ 'updateTaskData',
892
+ 'emitTaskConsultAccepted',
893
+ 'emitTaskConsulting',
894
+ ],
895
+ },
896
+
793
897
  // Participant leaves - handle conference downgrade scenarios
794
898
  [TaskEvent.PARTICIPANT_LEAVE]: [
795
899
  {
@@ -88,10 +88,20 @@ const isActiveConsultState = (taskData: TaskData | undefined, selfAgentId?: stri
88
88
  const hasConsultMedia = Object.values(taskData?.interaction?.media ?? {}).some(
89
89
  (media: any) => media?.mType === MEDIA_TYPE_CONSULT
90
90
  );
91
- const isPendingOrActiveSelfConsult =
92
- selfParticipant?.consultState === CONSULT_STATE.CONSULTING ||
93
- selfParticipant?.consultState === 'consultInitiated';
94
- if (isPendingOrActiveSelfConsult && hasConsultMedia && taskData?.isConsulted === false) {
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
+ ) {
95
105
  return true;
96
106
  }
97
107
  }
@@ -119,7 +129,42 @@ const isSelfConsultingOrPending = (
119
129
  );
120
130
  };
121
131
 
122
- const hasJoinedConsultDestination = (taskData: TaskData | undefined): boolean => {
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 => {
123
168
  if (!taskData?.interaction) return false;
124
169
  const participants = taskData.interaction.participants as any;
125
170
  const cpd = taskData.interaction.callProcessingDetails as any;
@@ -127,6 +172,20 @@ const hasJoinedConsultDestination = (taskData: TaskData | undefined): boolean =>
127
172
  if (backendSaysJoined) return true;
128
173
  if (!participants) return false;
129
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
+
130
189
  return Object.values(participants).some((p: any) => {
131
190
  if (!p || p.isConsulted !== true || p.hasLeft) return false;
132
191
 
@@ -154,7 +213,7 @@ const deriveConsultCallHeldFromTaskData = (taskData: TaskData | undefined): bool
154
213
  return Boolean(consultMedia.isHold);
155
214
  };
156
215
 
157
- const getTaskStateForUiControls = (
216
+ export const getTaskStateForUiControls = (
158
217
  taskData: TaskData | undefined,
159
218
  selfAgentId: string | undefined
160
219
  ): TaskState => {
@@ -214,11 +273,25 @@ const deriveTaskDataUpdates = (context: TaskContext, taskData: TaskData | undefi
214
273
  updates.consultDestinationAgentId = taskData.destAgentId;
215
274
  }
216
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
+
217
290
  if (consultingActive && taskData.destinationType) {
218
- updates.consultDestinationType = taskData.destinationType as DestinationType;
291
+ updates.consultDestinationType = mapConsultDestinationType(taskData.destinationType);
219
292
  }
220
293
 
221
- if (!context.consultInitiator) {
294
+ if (!isConsultTerminalEvent && !context.consultInitiator) {
222
295
  const consultInitiator = determineConsultInitiator(taskData, selfAgentId);
223
296
  if (consultInitiator !== undefined) {
224
297
  updates.consultInitiator = consultInitiator;
@@ -232,6 +305,7 @@ const deriveTaskDataUpdates = (context: TaskContext, taskData: TaskData | undefi
232
305
 
233
306
  const effectiveConsultInitiator = updates.consultInitiator ?? context.consultInitiator;
234
307
  if (
308
+ !isConsultTerminalEvent &&
235
309
  effectiveConsultInitiator &&
236
310
  conferenceFromPayload &&
237
311
  (consultingActive || selfConsultingOrPending || Boolean(taskData?.consultMediaResourceId))
@@ -239,16 +313,37 @@ const deriveTaskDataUpdates = (context: TaskContext, taskData: TaskData | undefi
239
313
  updates.consultFromConference = true;
240
314
  }
241
315
 
242
- if (consultingActive && taskData.interaction) {
243
- const joinedConsultee = hasJoinedConsultDestination(taskData);
244
- if (joinedConsultee) updates.consultDestinationAgentJoined = true;
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
+ }
245
337
 
246
338
  if (!context.consultDestinationType && !updates.consultDestinationType) {
247
339
  const hasEpDnParticipant = Boolean(
248
340
  taskData.interaction.participants &&
249
- Object.values(taskData.interaction.participants).some(
250
- (p: any) => p?.pType === 'EP-DN' && !p?.hasLeft
251
- )
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
+ })
252
347
  );
253
348
  if (hasEpDnParticipant) updates.consultDestinationType = 'entryPoint' as any;
254
349
  }
@@ -303,7 +398,7 @@ export function createInitialContext(
303
398
 
304
399
  export function updateUIControls(currentState: TaskState) {
305
400
  return assign(({context}: TaskActionArgs) => ({
306
- uiControls: computeUIControls(currentState, context),
401
+ uiControls: computeUIControls(currentState, context, context.taskData ?? undefined),
307
402
  }));
308
403
  }
309
404
 
@@ -349,7 +444,27 @@ export const actions: TaskActionsMap = {
349
444
  return {};
350
445
  }),
351
446
 
352
- handleConsultFailed: assign({consultDestinationAgentJoined: false, consultInitiator: false}),
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
+ }),
353
468
  handleConferenceStarted: assign({consultInitiator: false}),
354
469
 
355
470
  setConsultDestination: assign(({event}: TaskActionArgs) => {
@@ -423,15 +538,26 @@ export const actions: TaskActionsMap = {
423
538
  return {};
424
539
  }),
425
540
 
426
- clearConsultState: assign({
427
- consultDestinationType: null,
428
- consultDestinationAgentId: null,
429
- consultDestinationAgentJoined: false,
430
- consultInitiator: false,
431
- exitingConference: false,
432
- consultCallHeld: false,
433
- consultFromConference: false,
434
- transferConferenceRequested: false,
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
+ };
435
561
  }),
436
562
 
437
563
  setTransferConferenceRequested: assign({transferConferenceRequested: true}),