@webex/contact-center 3.12.0-next.122 → 3.12.0-next.124
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.
- package/.sdd/manifest.json +1 -1
- package/ai-docs/CONTRACTS.md +1 -1
- package/dist/services/task/TaskManager.js +137 -0
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/state-machine/TaskStateMachine.js +51 -23
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
- package/dist/services/task/voice/Voice.js +39 -1
- package/dist/services/task/voice/Voice.js.map +1 -1
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +86 -0
- package/dist/webex.js +1 -1
- package/package.json +2 -2
- package/src/services/task/TaskManager.ts +194 -1
- package/src/services/task/ai-docs/task-spec.md +3 -1
- package/src/services/task/state-machine/TaskStateMachine.ts +47 -20
- package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +18 -7
- package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +49 -9
- package/src/services/task/voice/Voice.ts +44 -2
- package/test/unit/spec/services/task/Task.ts +74 -6
- package/test/unit/spec/services/task/TaskManager.ts +416 -1
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +568 -0
- package/test/unit/spec/services/task/voice/Voice.ts +158 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -227,6 +227,15 @@ export declare function getTaskStateMachineConfig(uiControlConfig: UIControlConf
|
|
|
227
227
|
RESUME_RECORDING: {
|
|
228
228
|
actions: string[];
|
|
229
229
|
};
|
|
230
|
+
EXIT_CONFERENCE_SUCCESS: ({
|
|
231
|
+
guard: ({ context, event }: import("./guards").GuardParams) => boolean;
|
|
232
|
+
target: TaskState;
|
|
233
|
+
actions: string[];
|
|
234
|
+
} | {
|
|
235
|
+
target: TaskState;
|
|
236
|
+
actions: string[];
|
|
237
|
+
guard?: undefined;
|
|
238
|
+
})[];
|
|
230
239
|
};
|
|
231
240
|
};
|
|
232
241
|
HOLD_INITIATING: {
|
|
@@ -248,6 +257,10 @@ export declare function getTaskStateMachineConfig(uiControlConfig: UIControlConf
|
|
|
248
257
|
actions: string[];
|
|
249
258
|
guard?: undefined;
|
|
250
259
|
})[];
|
|
260
|
+
TASK_WRAPUP: {
|
|
261
|
+
target: TaskState;
|
|
262
|
+
actions: string[];
|
|
263
|
+
};
|
|
251
264
|
};
|
|
252
265
|
};
|
|
253
266
|
HELD: {
|
|
@@ -325,6 +338,15 @@ export declare function getTaskStateMachineConfig(uiControlConfig: UIControlConf
|
|
|
325
338
|
actions: string[];
|
|
326
339
|
guard?: undefined;
|
|
327
340
|
})[];
|
|
341
|
+
EXIT_CONFERENCE_SUCCESS: ({
|
|
342
|
+
guard: ({ context, event }: import("./guards").GuardParams) => boolean;
|
|
343
|
+
target: TaskState;
|
|
344
|
+
actions: string[];
|
|
345
|
+
} | {
|
|
346
|
+
target: TaskState;
|
|
347
|
+
actions: string[];
|
|
348
|
+
guard?: undefined;
|
|
349
|
+
})[];
|
|
328
350
|
};
|
|
329
351
|
};
|
|
330
352
|
RESUME_INITIATING: {
|
|
@@ -352,6 +374,10 @@ export declare function getTaskStateMachineConfig(uiControlConfig: UIControlConf
|
|
|
352
374
|
actions: string[];
|
|
353
375
|
guard?: undefined;
|
|
354
376
|
})[];
|
|
377
|
+
TASK_WRAPUP: {
|
|
378
|
+
target: TaskState;
|
|
379
|
+
actions: string[];
|
|
380
|
+
};
|
|
355
381
|
};
|
|
356
382
|
};
|
|
357
383
|
CONSULT_INITIATING: {
|
|
@@ -415,6 +441,10 @@ export declare function getTaskStateMachineConfig(uiControlConfig: UIControlConf
|
|
|
415
441
|
actions: string[];
|
|
416
442
|
guard?: undefined;
|
|
417
443
|
})[];
|
|
444
|
+
TASK_WRAPUP: {
|
|
445
|
+
target: TaskState;
|
|
446
|
+
actions: string[];
|
|
447
|
+
};
|
|
418
448
|
};
|
|
419
449
|
};
|
|
420
450
|
CONSULTING: {
|
|
@@ -544,6 +574,10 @@ export declare function getTaskStateMachineConfig(uiControlConfig: UIControlConf
|
|
|
544
574
|
actions: string[];
|
|
545
575
|
guard?: undefined;
|
|
546
576
|
})[];
|
|
577
|
+
TASK_WRAPUP: {
|
|
578
|
+
target: TaskState;
|
|
579
|
+
actions: string[];
|
|
580
|
+
};
|
|
547
581
|
};
|
|
548
582
|
};
|
|
549
583
|
CONFERENCING: {
|
|
@@ -660,6 +694,15 @@ export declare function getTaskStateMachineConfig(uiControlConfig: UIControlConf
|
|
|
660
694
|
WRAPPING_UP: {
|
|
661
695
|
entry: string[];
|
|
662
696
|
on: {
|
|
697
|
+
TASK_WRAPUP: ({
|
|
698
|
+
guard: ({ context }: {
|
|
699
|
+
context: any;
|
|
700
|
+
}) => boolean;
|
|
701
|
+
actions: string[];
|
|
702
|
+
} | {
|
|
703
|
+
actions: string[];
|
|
704
|
+
guard?: undefined;
|
|
705
|
+
})[];
|
|
663
706
|
WRAPUP_COMPLETE: {
|
|
664
707
|
target: TaskState;
|
|
665
708
|
actions: string[];
|
|
@@ -904,6 +947,15 @@ export declare function createTaskStateMachine(uiControlConfig: UIControlConfig,
|
|
|
904
947
|
RESUME_RECORDING: {
|
|
905
948
|
actions: string[];
|
|
906
949
|
};
|
|
950
|
+
EXIT_CONFERENCE_SUCCESS: ({
|
|
951
|
+
guard: ({ context, event }: import("./guards").GuardParams) => boolean;
|
|
952
|
+
target: TaskState;
|
|
953
|
+
actions: string[];
|
|
954
|
+
} | {
|
|
955
|
+
target: TaskState;
|
|
956
|
+
actions: string[];
|
|
957
|
+
guard?: undefined;
|
|
958
|
+
})[];
|
|
907
959
|
};
|
|
908
960
|
};
|
|
909
961
|
HOLD_INITIATING: {
|
|
@@ -925,6 +977,10 @@ export declare function createTaskStateMachine(uiControlConfig: UIControlConfig,
|
|
|
925
977
|
actions: string[];
|
|
926
978
|
guard?: undefined;
|
|
927
979
|
})[];
|
|
980
|
+
TASK_WRAPUP: {
|
|
981
|
+
target: TaskState;
|
|
982
|
+
actions: string[];
|
|
983
|
+
};
|
|
928
984
|
};
|
|
929
985
|
};
|
|
930
986
|
HELD: {
|
|
@@ -1002,6 +1058,15 @@ export declare function createTaskStateMachine(uiControlConfig: UIControlConfig,
|
|
|
1002
1058
|
actions: string[];
|
|
1003
1059
|
guard?: undefined;
|
|
1004
1060
|
})[];
|
|
1061
|
+
EXIT_CONFERENCE_SUCCESS: ({
|
|
1062
|
+
guard: ({ context, event }: import("./guards").GuardParams) => boolean;
|
|
1063
|
+
target: TaskState;
|
|
1064
|
+
actions: string[];
|
|
1065
|
+
} | {
|
|
1066
|
+
target: TaskState;
|
|
1067
|
+
actions: string[];
|
|
1068
|
+
guard?: undefined;
|
|
1069
|
+
})[];
|
|
1005
1070
|
};
|
|
1006
1071
|
};
|
|
1007
1072
|
RESUME_INITIATING: {
|
|
@@ -1029,6 +1094,10 @@ export declare function createTaskStateMachine(uiControlConfig: UIControlConfig,
|
|
|
1029
1094
|
actions: string[];
|
|
1030
1095
|
guard?: undefined;
|
|
1031
1096
|
})[];
|
|
1097
|
+
TASK_WRAPUP: {
|
|
1098
|
+
target: TaskState;
|
|
1099
|
+
actions: string[];
|
|
1100
|
+
};
|
|
1032
1101
|
};
|
|
1033
1102
|
};
|
|
1034
1103
|
CONSULT_INITIATING: {
|
|
@@ -1092,6 +1161,10 @@ export declare function createTaskStateMachine(uiControlConfig: UIControlConfig,
|
|
|
1092
1161
|
actions: string[];
|
|
1093
1162
|
guard?: undefined;
|
|
1094
1163
|
})[];
|
|
1164
|
+
TASK_WRAPUP: {
|
|
1165
|
+
target: TaskState;
|
|
1166
|
+
actions: string[];
|
|
1167
|
+
};
|
|
1095
1168
|
};
|
|
1096
1169
|
};
|
|
1097
1170
|
CONSULTING: {
|
|
@@ -1221,6 +1294,10 @@ export declare function createTaskStateMachine(uiControlConfig: UIControlConfig,
|
|
|
1221
1294
|
actions: string[];
|
|
1222
1295
|
guard?: undefined;
|
|
1223
1296
|
})[];
|
|
1297
|
+
TASK_WRAPUP: {
|
|
1298
|
+
target: TaskState;
|
|
1299
|
+
actions: string[];
|
|
1300
|
+
};
|
|
1224
1301
|
};
|
|
1225
1302
|
};
|
|
1226
1303
|
CONFERENCING: {
|
|
@@ -1337,6 +1414,15 @@ export declare function createTaskStateMachine(uiControlConfig: UIControlConfig,
|
|
|
1337
1414
|
WRAPPING_UP: {
|
|
1338
1415
|
entry: string[];
|
|
1339
1416
|
on: {
|
|
1417
|
+
TASK_WRAPUP: ({
|
|
1418
|
+
guard: ({ context }: {
|
|
1419
|
+
context: any;
|
|
1420
|
+
}) => boolean;
|
|
1421
|
+
actions: string[];
|
|
1422
|
+
} | {
|
|
1423
|
+
actions: string[];
|
|
1424
|
+
guard?: undefined;
|
|
1425
|
+
})[];
|
|
1340
1426
|
WRAPUP_COMPLETE: {
|
|
1341
1427
|
target: TaskState;
|
|
1342
1428
|
actions: string[];
|
package/dist/webex.js
CHANGED
package/package.json
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@types/platform": "1.3.4",
|
|
49
|
-
"@webex/calling": "3.12.0-next.
|
|
49
|
+
"@webex/calling": "3.12.0-next.99",
|
|
50
50
|
"@webex/internal-plugin-mercury": "3.12.0-next.51",
|
|
51
51
|
"@webex/internal-plugin-metrics": "3.12.0-next.47",
|
|
52
52
|
"@webex/internal-plugin-support": "3.12.0-next.54",
|
|
@@ -84,5 +84,5 @@
|
|
|
84
84
|
"typedoc": "^0.25.0",
|
|
85
85
|
"typescript": "5.4.5"
|
|
86
86
|
},
|
|
87
|
-
"version": "3.12.0-next.
|
|
87
|
+
"version": "3.12.0-next.124"
|
|
88
88
|
}
|
|
@@ -30,7 +30,7 @@ import TaskFactory from './TaskFactory';
|
|
|
30
30
|
import AnswerCallOnWebexService from '../AnswerCallOnWebexService';
|
|
31
31
|
import {getWebexCallingDeviceDetailsForAgent} from './WebexCallingUtils';
|
|
32
32
|
import WebRTC from './voice/WebRTC';
|
|
33
|
-
import {TaskEvent, type TaskEventPayload} from './state-machine';
|
|
33
|
+
import {TaskEvent, TaskState, type TaskEventPayload} from './state-machine';
|
|
34
34
|
import {MEDIA_TYPE_MAIN_CALL} from './state-machine/constants';
|
|
35
35
|
import {normalizeTaskData} from './taskDataNormalizer';
|
|
36
36
|
import {ApiAIAssistant} from '../ApiAiAssistant';
|
|
@@ -44,6 +44,21 @@ const MAIN_INTERACTION_CORRELATED_EVENTS = new Set<CC_EVENTS>([
|
|
|
44
44
|
|
|
45
45
|
const isCcEvent = (value: string): value is CC_EVENTS => CC_EVENT_SET.has(value as CC_EVENTS);
|
|
46
46
|
|
|
47
|
+
const WRAP_UP_STAMP_EVENTS = new Set<CC_EVENTS>([
|
|
48
|
+
CC_EVENTS.AGENT_WRAPUP,
|
|
49
|
+
CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED,
|
|
50
|
+
CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
51
|
+
CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED,
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const PARTICIPANT_LEAVE_WRAP_STATES = new Set<TaskState>([
|
|
55
|
+
TaskState.HELD,
|
|
56
|
+
TaskState.RESUME_INITIATING,
|
|
57
|
+
TaskState.CONSULTING,
|
|
58
|
+
TaskState.CONSULT_INITIATING,
|
|
59
|
+
TaskState.CONFERENCING,
|
|
60
|
+
]);
|
|
61
|
+
|
|
47
62
|
/** @internal */
|
|
48
63
|
export default class TaskManager extends EventEmitter {
|
|
49
64
|
private call: ICall;
|
|
@@ -195,6 +210,175 @@ export default class TaskManager extends EventEmitter {
|
|
|
195
210
|
this.webCallingService.off(LINE_EVENTS.INCOMING_CALL, this.handleIncomingWebCall);
|
|
196
211
|
}
|
|
197
212
|
|
|
213
|
+
/**
|
|
214
|
+
* True when this agent left on the current payload.
|
|
215
|
+
* A named other participantId is never self-leave (even if the participants
|
|
216
|
+
* map is partial). Missing-map fallback is EP-DN only: participantId absent
|
|
217
|
+
* and self omitted now, with previous-task evidence when a prior map exists.
|
|
218
|
+
*/
|
|
219
|
+
private static didSelfLeaveConference(
|
|
220
|
+
payload: WebSocketPayload,
|
|
221
|
+
agentId?: string,
|
|
222
|
+
task?: ITask
|
|
223
|
+
): boolean {
|
|
224
|
+
if (!agentId) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
if (payload.participantId === agentId) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
if (payload.participantId && payload.participantId !== agentId) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const participants = payload.interaction?.participants;
|
|
235
|
+
if (!participants || agentId in participants) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const previousParticipants = task?.data?.interaction?.participants;
|
|
240
|
+
if (previousParticipants && !(agentId in previousParticipants)) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Consulted non-owner: Voice exitConference / shouldWrapUpForThisAgent stays
|
|
249
|
+
* false unless an explicit wrap-up signal includes this agent.
|
|
250
|
+
*/
|
|
251
|
+
private static isConsultedNonOwner(
|
|
252
|
+
payload: WebSocketPayload,
|
|
253
|
+
agentId?: string,
|
|
254
|
+
task?: ITask
|
|
255
|
+
): boolean {
|
|
256
|
+
const isConsulted = payload.isConsulted ?? task?.data?.isConsulted;
|
|
257
|
+
if (isConsulted !== true) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
const owner = payload.interaction?.owner ?? task?.data?.interaction?.owner;
|
|
261
|
+
|
|
262
|
+
return Boolean(agentId && owner && owner !== agentId);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private static hasNonemptyPendingWrapUp(payload: WebSocketPayload): boolean {
|
|
266
|
+
return Array.isArray(payload.agentsPendingWrapUp) && payload.agentsPendingWrapUp.length > 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Transfer initiator fallback: widgets set transferConferenceRequested on the
|
|
271
|
+
* actor; desktop success payloads may include consultingAgentId. Only used
|
|
272
|
+
* when agentsPendingWrapUp is empty or absent.
|
|
273
|
+
*/
|
|
274
|
+
private static isSelfConferenceTransferInitiator(
|
|
275
|
+
payload: WebSocketPayload,
|
|
276
|
+
agentId?: string,
|
|
277
|
+
task?: ITask
|
|
278
|
+
): boolean {
|
|
279
|
+
const snapshotContext = task?.stateMachineService?.getSnapshot?.()?.context as
|
|
280
|
+
| {transferConferenceRequested?: boolean}
|
|
281
|
+
| undefined;
|
|
282
|
+
if (snapshotContext?.transferConferenceRequested === true) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return Boolean(agentId && payload.consultingAgentId === agentId);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Explicit wrap-up signals only (no owner / isConsulted fallback).
|
|
291
|
+
* Used for events that remaining conference agents also receive.
|
|
292
|
+
* nonempty agentsPendingWrapUp is authoritative, then wrapUpRequired / isWrapUp.
|
|
293
|
+
*/
|
|
294
|
+
private static wrapUpRequiredFromExplicitSignals(
|
|
295
|
+
payload: WebSocketPayload,
|
|
296
|
+
agentId?: string
|
|
297
|
+
): boolean {
|
|
298
|
+
const pending = payload.agentsPendingWrapUp;
|
|
299
|
+
if (Array.isArray(pending) && pending.length > 0) {
|
|
300
|
+
return Boolean(agentId && pending.includes(agentId));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (payload.wrapUpRequired === true) {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
if (!agentId) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return payload.interaction?.participants?.[agentId]?.isWrapUp === true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Stamp wrapUpRequired once at ingress (prepareEventContext) so updateTaskData
|
|
315
|
+
* and the mapper see the same flag. Remaining agents stay false.
|
|
316
|
+
*/
|
|
317
|
+
private static stampWrapUpRequiredForEvent(
|
|
318
|
+
eventType: CC_EVENTS,
|
|
319
|
+
payload: WebSocketPayload,
|
|
320
|
+
agentId?: string,
|
|
321
|
+
task?: ITask
|
|
322
|
+
): WebSocketPayload {
|
|
323
|
+
if (!WRAP_UP_STAMP_EVENTS.has(eventType)) {
|
|
324
|
+
return payload;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (eventType === CC_EVENTS.AGENT_WRAPUP) {
|
|
328
|
+
return {...payload, wrapUpRequired: true};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (eventType === CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED) {
|
|
332
|
+
return {
|
|
333
|
+
...payload,
|
|
334
|
+
wrapUpRequired: TaskManager.wrapUpRequiredFromExplicitSignals(payload, agentId),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (eventType === CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE) {
|
|
339
|
+
const actorState = task?.stateMachineService?.getSnapshot?.()?.value;
|
|
340
|
+
if (actorState && !PARTICIPANT_LEAVE_WRAP_STATES.has(actorState as TaskState)) {
|
|
341
|
+
return payload;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const left = TaskManager.didSelfLeaveConference(payload, agentId, task);
|
|
345
|
+
if (!left) {
|
|
346
|
+
return {
|
|
347
|
+
...payload,
|
|
348
|
+
wrapUpRequired: TaskManager.wrapUpRequiredFromExplicitSignals(payload, agentId),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
if (TaskManager.hasNonemptyPendingWrapUp(payload)) {
|
|
352
|
+
return {
|
|
353
|
+
...payload,
|
|
354
|
+
wrapUpRequired: Boolean(agentId && payload.agentsPendingWrapUp?.includes(agentId)),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
...payload,
|
|
360
|
+
wrapUpRequired:
|
|
361
|
+
TaskManager.wrapUpRequiredFromExplicitSignals(payload, agentId) ||
|
|
362
|
+
!TaskManager.isConsultedNonOwner(payload, agentId, task),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// AGENT_CONFERENCE_TRANSFERRED — pending list wins; initiator only if empty/absent
|
|
367
|
+
if (TaskManager.hasNonemptyPendingWrapUp(payload)) {
|
|
368
|
+
return {
|
|
369
|
+
...payload,
|
|
370
|
+
wrapUpRequired: TaskManager.wrapUpRequiredFromExplicitSignals(payload, agentId),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
...payload,
|
|
376
|
+
wrapUpRequired:
|
|
377
|
+
TaskManager.wrapUpRequiredFromExplicitSignals(payload, agentId) ||
|
|
378
|
+
TaskManager.isSelfConferenceTransferInitiator(payload, agentId, task),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
198
382
|
/**
|
|
199
383
|
* Map WebSocket CC_EVENTS to state machine TaskEvent
|
|
200
384
|
* @param ccEvent - The CC_EVENT type from WebSocket
|
|
@@ -645,6 +829,15 @@ export default class TaskManager extends EventEmitter {
|
|
|
645
829
|
adjustedPayload = TaskManager.preserveConfirmedOwner(task, adjustedPayload);
|
|
646
830
|
}
|
|
647
831
|
|
|
832
|
+
if (WRAP_UP_STAMP_EVENTS.has(eventType)) {
|
|
833
|
+
adjustedPayload = TaskManager.stampWrapUpRequiredForEvent(
|
|
834
|
+
eventType,
|
|
835
|
+
adjustedPayload,
|
|
836
|
+
this.agentId,
|
|
837
|
+
task
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
|
|
648
841
|
const stateMachineEvent = TaskManager.mapEventToTaskStateMachineEvent(
|
|
649
842
|
eventType,
|
|
650
843
|
adjustedPayload,
|
|
@@ -438,6 +438,7 @@ The public `TASK_EVENTS` enum contains 49 members; every member is listed below
|
|
|
438
438
|
| TASK-R-008 | Voice `dropConferenceParticipant` must validate the public target, resolve the latest main interaction, POST an empty body to the encoded participant-drop route, and settle only from `ParticipantLeftConference`, `ParticipantDropConferenceFailed`, HTTP failure, or the existing AQM timeout. | Participant removal is backend-authoritative and must preserve event-driven roster/state synchronization without exposing participant identifiers through telemetry or logs. | `src/services/task/voice/Voice.ts`, `src/services/task/contact.ts`, `src/services/task/types.ts` | `test/unit/spec/services/task/voice/Voice.ts`, `test/unit/spec/services/task/contact.ts`, `test/unit/spec/services/core/aqm-reqs.ts` | Backend authorization and media removal are remote-service responsibilities. | PRESENT |
|
|
439
439
|
| TASK-R-009 | ContactCenter must inject Desktop Profile collaboration policy into TaskFactory, and every created voice/digital Task must expose ordered Consult and Transfer destination categories through `TaskUIControls.consultTransferDestinations`. | Task consumers need one already-computed policy surface and must not fetch profile flags or duplicate destination decisions. | `src/cc.ts`, `src/services/task/TaskFactory.ts`, `src/services/task/types.ts` | `test/unit/spec/cc.ts`, `test/unit/spec/services/task/TaskFactory.ts` | Host-specific UI options may only further hide an SDK-allowed category. | PRESENT |
|
|
440
440
|
| TASK-R-010 | TaskManager must propagate backend owner promotion without electing an owner locally. `ContactOwnerChanged` resolves an exact task first, then one unique related task whose existing snapshot proves that the current Agent is active on `mainCall`, or whose incoming snapshot both names that Agent as `interaction.owner` and proves the same active-main-leg membership. Active membership requires a participant entry with `hasLeft !== true`. If no task exists, recovery is limited to that incoming owner=current-Agent proof: create the normal Task under the stable main interaction ID, silently HYDRATE its actor before listener installation, then process the original owner-change event so consumers receive one `task:hydrate` and no `task:incoming`. An owner-changing `ContactUpdated` uses the existing owner-change hydrate path only for an existing task, and a late `ParticipantLeftConference` cannot replace a confirmed active owner with the departed participant. | Every surviving agent must observe the backend-selected primary Agent immediately, including stale child-keyed and narrowly recoverable desynchronization cases, without duplicating tasks or exposing an uninitialized actor. | `src/services/task/TaskManager.ts` | `test/unit/spec/services/task/TaskManager.ts`, `test/unit/spec/services/task/Task.ts` | Backend owner selection and delivery of a complete owner-bearing `ContactOwnerChanged` payload remain remote responsibilities; missing-task `ContactUpdated` remains update-only. | PRESENT |
|
|
441
|
+
| TASK-R-011 | TaskManager must stamp `wrapUpRequired` once in `prepareEventContext` on `AGENT_WRAPUP`, `AGENT_CONSULT_CONFERENCE_ENDED`, `PARTICIPANT_LEFT_CONFERENCE`, and `AGENT_CONFERENCE_TRANSFERRED` before `updateTaskData` / mapper (mapper passes the already-stamped payload through). Voice `exitConference` stamps the same flag on `EXIT_CONFERENCE_SUCCESS` (that HTTP path skips TaskManager) using `shouldWrapUpForThisAgent`, then the same empty-pending self-exit fallback as leave (true unless this agent is a consulted non-owner). Voice pending-list precedence uses the correlated HTTP response only (a cached `agentsPendingWrapUp` is ignored when the response omits the list). `AGENT_WRAPUP` is always `true`. Conference-end uses explicit signals only (`agentsPendingWrapUp`, then `wrapUpRequired` / `isWrapUp`). A nonempty `agentsPendingWrapUp` is authoritative and wins over initiator fallback: `consultingAgentId === self` and actor `transferConferenceRequested` are honored only when the list is empty or absent. On participant leave: do not stamp when the actor ignores `PARTICIPANT_LEAVE` (`CONNECTED`, `HOLD_INITIATING`, `CONF_INITIATING`); stamp only in `HELD`, `RESUME_INITIATING`, `CONSULTING`, `CONSULT_INITIATING`, or `CONFERENCING` (or when no actor snapshot exists). A named other `participantId` is never self-leave (do not infer leave from a partial `participants` map); missing-map fallback applies only when `participantId` is absent (EP-DN), and when a previous task map exists it must have contained self. Remaining agents use explicit signals only; this agent left + nonempty pending uses `includes(self)`; this agent left + empty/absent pending stamps `true` unless this agent is a consulted non-owner. Remaining conference agents stay `false`. `setupAutoWrapupTimer()` still requires profile `autoWrapup !== false`, a valid interval, and a default reason. `PARTICIPANT_LEAVE` and Voice `EXIT_CONFERENCE_SUCCESS` into `WRAPPING_UP` do not transition-emit; the `WRAPPING_UP` entry still emits `task:wrapup`. `EXIT_CONFERENCE_SUCCESS` is handled in `CONNECTED`, `HELD`, and `CONFERENCING`. `TASK_WRAPUP` is handled in `HOLD_INITIATING`, `RESUME_INITIATING`, `CONSULT_INITIATING`, and `CONF_INITIATING` (transition `updateTaskData` + `markEnded` only). Late `TASK_WRAPUP` in `WRAPPING_UP` always runs `updateTaskData` and runs `emitTaskWrapup` only when `context.taskData.wrapUpRequired` was not already `true`. | Consumers rely on `task:wrapup` and `task.autoWrapup` when wrap-up starts; those surfaces only activate when ingress task data already has `wrapUpRequired === true`. | `src/services/task/TaskManager.ts`, `src/services/task/voice/Voice.ts`, `src/services/task/Task.ts`, `src/services/task/state-machine/TaskStateMachine.ts` | `test/unit/spec/services/task/TaskManager.ts`, `test/unit/spec/services/task/voice/Voice.ts`, `test/unit/spec/services/task/Task.ts`, `test/unit/spec/services/task/state-machine/TaskStateMachine.ts` | Profile auto wrap-up, interval, and default reason remain agent-config responsibilities. | PRESENT |
|
|
441
442
|
|
|
442
443
|
## Design Overview
|
|
443
444
|
Task separates its stable consumption boundary from collaborators so ownership and failure behavior stay explicit. A shared Task base preserves a stable API while media-specific subclasses and a separate state engine enforce capability differences.
|
|
@@ -452,7 +453,7 @@ Task separates its stable consumption boundary from collaborators so ownership a
|
|
|
452
453
|
| `facebook` | Unsupported by TaskFactory; throws `Unknown media type` |
|
|
453
454
|
| `whatsapp` | Unsupported by TaskFactory; throws `Unknown media type` |
|
|
454
455
|
|
|
455
|
-
If enabled in agent profile, wrapup completes automatically after timeout
|
|
456
|
+
If enabled in agent profile, wrapup completes automatically after timeout. The timer starts only when ingress task data has `wrapUpRequired === true` (TASK-R-011).
|
|
456
457
|
|
|
457
458
|
> **Host-application example:** `updateWrapupStatus` is a consumer-owned UI callback, not SDK package logging.
|
|
458
459
|
|
|
@@ -1432,6 +1433,7 @@ Use `test/unit/spec/services/task/Task.ts`, `TaskFactory.ts`, `TaskManager.ts`,
|
|
|
1432
1433
|
| `TASK-R-008` | `test/unit/spec/services/task/Task.ts`, `test/unit/spec/services/task/voice/Voice.ts`, `test/unit/spec/services/task/contact.ts`, `test/unit/spec/services/core/aqm-reqs.ts`, `test/unit/spec/services/task/TaskManager.ts` | None. |
|
|
1433
1434
|
| `TASK-R-009` | `test/unit/spec/cc.ts`, `test/unit/spec/services/task/TaskFactory.ts`, `test/unit/spec/services/task/state-machine/uiControlsComputer.ts` | None. |
|
|
1434
1435
|
| `TASK-R-010` | `test/unit/spec/services/task/TaskManager.ts`, `test/unit/spec/services/task/Task.ts` | None. |
|
|
1436
|
+
| `TASK-R-011` | `test/unit/spec/services/task/TaskManager.ts`, `test/unit/spec/services/task/voice/Voice.ts`, `test/unit/spec/services/task/Task.ts`, `test/unit/spec/services/task/state-machine/TaskStateMachine.ts` | None. |
|
|
1435
1437
|
|
|
1436
1438
|
## Traceability
|
|
1437
1439
|
- Repo architecture: `../../../../ai-docs/ARCHITECTURE.md` · Registry: `../../../../ai-docs/SPEC_INDEX.md`
|
|
@@ -73,7 +73,6 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
73
73
|
'markEnded',
|
|
74
74
|
'clearConsultState',
|
|
75
75
|
'emitTaskParticipantLeft',
|
|
76
|
-
'emitTaskWrapup',
|
|
77
76
|
],
|
|
78
77
|
},
|
|
79
78
|
{
|
|
@@ -90,6 +89,18 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
90
89
|
},
|
|
91
90
|
];
|
|
92
91
|
|
|
92
|
+
const exitConferenceSuccessTransitions = () => [
|
|
93
|
+
{
|
|
94
|
+
guard: guards.shouldWrapUp,
|
|
95
|
+
target: TaskState.WRAPPING_UP,
|
|
96
|
+
actions: ['updateTaskData', 'markEnded', 'clearConsultState'],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
target: TaskState.TERMINATED,
|
|
100
|
+
actions: ['updateTaskData', 'markEnded', 'clearConsultState', 'emitTaskEnd'],
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
93
104
|
const currentAgentConsultEndTransitions = () => [
|
|
94
105
|
{
|
|
95
106
|
guard: (params) =>
|
|
@@ -252,7 +263,7 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
252
263
|
{
|
|
253
264
|
guard: guards.shouldWrapUp,
|
|
254
265
|
target: TaskState.WRAPPING_UP,
|
|
255
|
-
actions: ['updateTaskData', 'markEnded'
|
|
266
|
+
actions: ['updateTaskData', 'markEnded'],
|
|
256
267
|
},
|
|
257
268
|
{
|
|
258
269
|
target: TaskState.TERMINATED,
|
|
@@ -444,7 +455,7 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
444
455
|
],
|
|
445
456
|
[TaskEvent.TASK_WRAPUP]: {
|
|
446
457
|
target: TaskState.WRAPPING_UP,
|
|
447
|
-
actions: ['updateTaskData', 'markEnded'
|
|
458
|
+
actions: ['updateTaskData', 'markEnded'],
|
|
448
459
|
},
|
|
449
460
|
[TaskEvent.OUTBOUND_FAILED]: [
|
|
450
461
|
{
|
|
@@ -463,6 +474,7 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
463
474
|
[TaskEvent.RESUME_RECORDING]: {
|
|
464
475
|
actions: ['updateTaskData', 'setRecordingState', 'emitTaskRecordingResumed'],
|
|
465
476
|
},
|
|
477
|
+
[TaskEvent.EXIT_CONFERENCE_SUCCESS]: exitConferenceSuccessTransitions(),
|
|
466
478
|
},
|
|
467
479
|
},
|
|
468
480
|
|
|
@@ -489,6 +501,10 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
489
501
|
actions: ['updateTaskData', 'markEnded', 'emitTaskOutdialFailed', 'emitTaskEnd'],
|
|
490
502
|
},
|
|
491
503
|
],
|
|
504
|
+
[TaskEvent.TASK_WRAPUP]: {
|
|
505
|
+
target: TaskState.WRAPPING_UP,
|
|
506
|
+
actions: ['updateTaskData', 'markEnded'],
|
|
507
|
+
},
|
|
492
508
|
},
|
|
493
509
|
},
|
|
494
510
|
|
|
@@ -586,7 +602,7 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
586
602
|
// TODO: This may not be a valid transition, this needs to be checked as well
|
|
587
603
|
[TaskEvent.TASK_WRAPUP]: {
|
|
588
604
|
target: TaskState.WRAPPING_UP,
|
|
589
|
-
actions: ['updateTaskData', 'markEnded'
|
|
605
|
+
actions: ['updateTaskData', 'markEnded'],
|
|
590
606
|
},
|
|
591
607
|
[TaskEvent.OUTBOUND_FAILED]: [
|
|
592
608
|
{
|
|
@@ -599,6 +615,7 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
599
615
|
actions: ['updateTaskData', 'markEnded', 'emitTaskOutdialFailed', 'emitTaskEnd'],
|
|
600
616
|
},
|
|
601
617
|
],
|
|
618
|
+
[TaskEvent.EXIT_CONFERENCE_SUCCESS]: exitConferenceSuccessTransitions(),
|
|
602
619
|
},
|
|
603
620
|
},
|
|
604
621
|
|
|
@@ -626,6 +643,10 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
626
643
|
actions: ['updateTaskData', 'markEnded', 'emitTaskOutdialFailed', 'emitTaskEnd'],
|
|
627
644
|
},
|
|
628
645
|
],
|
|
646
|
+
[TaskEvent.TASK_WRAPUP]: {
|
|
647
|
+
target: TaskState.WRAPPING_UP,
|
|
648
|
+
actions: ['updateTaskData', 'markEnded'],
|
|
649
|
+
},
|
|
629
650
|
},
|
|
630
651
|
},
|
|
631
652
|
|
|
@@ -714,6 +735,10 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
714
735
|
actions: ['updateTaskData', 'markEnded', 'emitTaskOutdialFailed', 'emitTaskEnd'],
|
|
715
736
|
},
|
|
716
737
|
],
|
|
738
|
+
[TaskEvent.TASK_WRAPUP]: {
|
|
739
|
+
target: TaskState.WRAPPING_UP,
|
|
740
|
+
actions: ['updateTaskData', 'markEnded'],
|
|
741
|
+
},
|
|
717
742
|
},
|
|
718
743
|
},
|
|
719
744
|
|
|
@@ -893,7 +918,6 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
893
918
|
'clearConsultState',
|
|
894
919
|
'handleTransferConferenceSuccess',
|
|
895
920
|
'clearTransferConferenceRequested',
|
|
896
|
-
'emitTaskWrapup',
|
|
897
921
|
],
|
|
898
922
|
},
|
|
899
923
|
{
|
|
@@ -941,7 +965,7 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
941
965
|
},
|
|
942
966
|
[TaskEvent.TASK_WRAPUP]: {
|
|
943
967
|
target: TaskState.WRAPPING_UP,
|
|
944
|
-
actions: ['updateTaskData', 'markEnded', 'clearConsultState'
|
|
968
|
+
actions: ['updateTaskData', 'markEnded', 'clearConsultState'],
|
|
945
969
|
},
|
|
946
970
|
[TaskEvent.OUTBOUND_FAILED]: [
|
|
947
971
|
{
|
|
@@ -1015,6 +1039,10 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
1015
1039
|
actions: ['updateTaskData', 'markEnded', 'emitTaskOutdialFailed', 'emitTaskEnd'],
|
|
1016
1040
|
},
|
|
1017
1041
|
],
|
|
1042
|
+
[TaskEvent.TASK_WRAPUP]: {
|
|
1043
|
+
target: TaskState.WRAPPING_UP,
|
|
1044
|
+
actions: ['updateTaskData', 'markEnded'],
|
|
1045
|
+
},
|
|
1018
1046
|
},
|
|
1019
1047
|
},
|
|
1020
1048
|
|
|
@@ -1028,17 +1056,7 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
1028
1056
|
'emitTaskConferenceStarted',
|
|
1029
1057
|
],
|
|
1030
1058
|
},
|
|
1031
|
-
[TaskEvent.EXIT_CONFERENCE_SUCCESS]:
|
|
1032
|
-
{
|
|
1033
|
-
guard: guards.shouldWrapUp,
|
|
1034
|
-
target: TaskState.WRAPPING_UP,
|
|
1035
|
-
actions: ['updateTaskData', 'markEnded', 'clearConsultState', 'emitTaskWrapup'],
|
|
1036
|
-
},
|
|
1037
|
-
{
|
|
1038
|
-
target: TaskState.TERMINATED,
|
|
1039
|
-
actions: ['updateTaskData', 'markEnded', 'clearConsultState', 'emitTaskEnd'],
|
|
1040
|
-
},
|
|
1041
|
-
],
|
|
1059
|
+
[TaskEvent.EXIT_CONFERENCE_SUCCESS]: exitConferenceSuccessTransitions(),
|
|
1042
1060
|
|
|
1043
1061
|
// Needed as all agents in conference get this event, hence we need to clear the consult state
|
|
1044
1062
|
[TaskEvent.CONSULT_END]: [
|
|
@@ -1156,7 +1174,6 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
1156
1174
|
'clearConsultState',
|
|
1157
1175
|
'handleTransferConferenceSuccess',
|
|
1158
1176
|
'clearTransferConferenceRequested',
|
|
1159
|
-
'emitTaskWrapup',
|
|
1160
1177
|
],
|
|
1161
1178
|
},
|
|
1162
1179
|
{
|
|
@@ -1191,7 +1208,7 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
1191
1208
|
// Agent who should wrap up → WRAPPING_UP
|
|
1192
1209
|
guard: guards.shouldWrapUp,
|
|
1193
1210
|
target: TaskState.WRAPPING_UP,
|
|
1194
|
-
actions: ['updateTaskData', 'markEnded', 'clearConsultState'
|
|
1211
|
+
actions: ['updateTaskData', 'markEnded', 'clearConsultState'],
|
|
1195
1212
|
},
|
|
1196
1213
|
{
|
|
1197
1214
|
// Customer still in call → CONNECTED
|
|
@@ -1229,7 +1246,7 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
1229
1246
|
[TaskEvent.TASK_WRAPUP]: {
|
|
1230
1247
|
guard: guards.shouldWrapUp,
|
|
1231
1248
|
target: TaskState.WRAPPING_UP,
|
|
1232
|
-
actions: ['updateTaskData', 'markEnded', 'clearConsultState'
|
|
1249
|
+
actions: ['updateTaskData', 'markEnded', 'clearConsultState'],
|
|
1233
1250
|
},
|
|
1234
1251
|
[TaskEvent.OUTBOUND_FAILED]: [
|
|
1235
1252
|
{
|
|
@@ -1249,6 +1266,16 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
|
1249
1266
|
// Only emit wrapup event on entry - task:end should only be emitted when COMPLETED
|
|
1250
1267
|
entry: ['emitTaskWrapup'],
|
|
1251
1268
|
on: {
|
|
1269
|
+
// Late AgentWrapup after conference exit already entered WRAPPING_UP
|
|
1270
|
+
[TaskEvent.TASK_WRAPUP]: [
|
|
1271
|
+
{
|
|
1272
|
+
guard: ({context}) => context.taskData?.wrapUpRequired === true,
|
|
1273
|
+
actions: ['updateTaskData'],
|
|
1274
|
+
},
|
|
1275
|
+
{
|
|
1276
|
+
actions: ['updateTaskData', 'emitTaskWrapup'],
|
|
1277
|
+
},
|
|
1278
|
+
],
|
|
1252
1279
|
// AgentWrappedup Event
|
|
1253
1280
|
[TaskEvent.WRAPUP_COMPLETE]: {
|
|
1254
1281
|
target: TaskState.COMPLETED,
|