@xema/omni-protocol 0.1.98 → 0.1.99
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/dist/index.d.ts +128 -106
- package/dist/index.js +5 -15
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +41 -27
- package/dist/validation.d.ts +20 -6
- package/dist/validation.js +175 -202
- package/guide.md +235 -284
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -232,25 +232,6 @@ export interface AuthenticationFailure {
|
|
|
232
232
|
/** Names a declared credentials field when the failure belongs to one. */
|
|
233
233
|
field?: string;
|
|
234
234
|
}
|
|
235
|
-
/** What a lead may do with their team. Declared by presence. */
|
|
236
|
-
export interface TeamCapabilities {
|
|
237
|
-
/**
|
|
238
|
-
* This lead may act on their team's breaks through `executeTeamBreak` -- force-break, end-forced-break, decide-break-request,
|
|
239
|
-
* set-break-policy -- as far as the provider supports; a command it lacks answers
|
|
240
|
-
* `omni.capability-not-enabled`. Requires `executeTeamBreak`.
|
|
241
|
-
*/
|
|
242
|
-
breakControl?: true;
|
|
243
|
-
/** This lead may join a member's call on request. Requires `executeTeamLeadAssist`. */
|
|
244
|
-
leadAssistControl?: true;
|
|
245
|
-
/**
|
|
246
|
-
* This lead may listen to a member's call unasked, in these modes and no others. Coach and
|
|
247
|
-
* join-call begin with silent listening, so the list always includes `listen`. Nothing of it reaches the
|
|
248
|
-
* member's task. Requires `executeTeamListen`.
|
|
249
|
-
*/
|
|
250
|
-
listeningControl?: ListeningMode[];
|
|
251
|
-
/** This lead sets the team's policy per capability -- on, off, or the agent's -- within what the queue allows. Requires `executeTeamPolicy`. */
|
|
252
|
-
policyControl?: true;
|
|
253
|
-
}
|
|
254
235
|
/**
|
|
255
236
|
* How a lead listening to a member's call is heard: `listen` hears both sides in silence,
|
|
256
237
|
* `coach` is heard by the agent alone, `join-call` by everyone on the call.
|
|
@@ -267,8 +248,14 @@ export interface UserCapabilities {
|
|
|
267
248
|
breaks?: true;
|
|
268
249
|
/** The choices the team left to this person, with where each stands. Omitted when there are none. Requires `setPreference`. */
|
|
269
250
|
preferences?: AgentPreference[];
|
|
270
|
-
/**
|
|
271
|
-
|
|
251
|
+
/**
|
|
252
|
+
* This login leads a team. The flag alone turns on the team feature in the agent application:
|
|
253
|
+
* the lead sees their members with their tasks and stats, and acts on them through `executeTeam`
|
|
254
|
+
* with the authority the flag carries. No per-action permission rides beside it. While the lead
|
|
255
|
+
* has the feature on, a `TeamMembers` object is published to them on every snapshot, `members: []`
|
|
256
|
+
* included; to nobody else, ever.
|
|
257
|
+
*/
|
|
258
|
+
lead?: true;
|
|
272
259
|
}
|
|
273
260
|
/**
|
|
274
261
|
* Only `authenticated` and `refreshing` know who the agent is and what they may do, and only
|
|
@@ -668,6 +655,7 @@ export type TaskCapabilities<C extends Channel = Channel> = C extends "voice" ?
|
|
|
668
655
|
hold?: Lockable<true>;
|
|
669
656
|
/** The provider ends the caller connection and agent-added channels owned by this interaction, including inherited channels after transfer/takeover. Interaction completion remains separate. */
|
|
670
657
|
endCall?: Lockable<true>;
|
|
658
|
+
terminateCall?: Lockable<true>;
|
|
671
659
|
/** Connect back to the party while `completing`, whoever placed the call; the task returns to `in-progress`. */
|
|
672
660
|
connectBack?: Lockable<true>;
|
|
673
661
|
coldTransfer?: Lockable<DestinationDirectory>;
|
|
@@ -875,27 +863,13 @@ export type TaskLeadAssist = {
|
|
|
875
863
|
leadId: UserId;
|
|
876
864
|
});
|
|
877
865
|
/**
|
|
878
|
-
* On
|
|
879
|
-
*
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
memberId: UserId;
|
|
883
|
-
/** The member's assignment the lead joined: the lead's task has an assignment of its own. */
|
|
884
|
-
assignmentId: AssignmentId;
|
|
885
|
-
note?: string;
|
|
886
|
-
since: IsoTimestamp;
|
|
887
|
-
}
|
|
888
|
-
/**
|
|
889
|
-
* On the lead's own task while they listen to a member's call: whose call, which call, and how the
|
|
890
|
-
* lead is heard, restated on every change of mode. Nothing of it reaches the member's task. There
|
|
891
|
-
* is no take-over here; a lead who wants the call uses lead assist. It is the lead's only task:
|
|
892
|
-
* a lead listens while holding no work of their own, idle or on a break of a kind in
|
|
893
|
-
* `LISTENING_BREAK_KINDS`.
|
|
866
|
+
* On a task that reached this agent by a lead's take-over: which member the call was taken from,
|
|
867
|
+
* and when. The provider moves the call from one agent to another, and the lead is the new agent,
|
|
868
|
+
* so the task is an ordinary assignment with the call's history -- offered whatever break the lead
|
|
869
|
+
* is on, counted against no capacity, provided their voice channel is free.
|
|
894
870
|
*/
|
|
895
|
-
export interface
|
|
871
|
+
export interface TaskTakenOver {
|
|
896
872
|
memberId: UserId;
|
|
897
|
-
assignmentId: AssignmentId;
|
|
898
|
-
mode: ListeningMode;
|
|
899
873
|
since: IsoTimestamp;
|
|
900
874
|
}
|
|
901
875
|
/**
|
|
@@ -995,15 +969,13 @@ export type Task<C extends Channel = Channel> = {
|
|
|
995
969
|
};
|
|
996
970
|
onCall?: OnCall[];
|
|
997
971
|
leadAssist?: TaskLeadAssist;
|
|
998
|
-
|
|
999
|
-
listening?: TaskListening;
|
|
972
|
+
takenOver?: TaskTakenOver;
|
|
1000
973
|
media?: TaskMediaState;
|
|
1001
974
|
} : {
|
|
1002
975
|
recording?: never;
|
|
1003
976
|
onCall?: never;
|
|
1004
977
|
leadAssist?: never;
|
|
1005
|
-
|
|
1006
|
-
listening?: never;
|
|
978
|
+
takenOver?: never;
|
|
1007
979
|
media?: never;
|
|
1008
980
|
});
|
|
1009
981
|
/**
|
|
@@ -1022,7 +994,7 @@ export type TaskOutcome = {
|
|
|
1022
994
|
type: "transferred";
|
|
1023
995
|
destinationId?: string;
|
|
1024
996
|
}
|
|
1025
|
-
/** A lead took the call over
|
|
997
|
+
/** A lead took the call over and the agent has wrapped it: the outcome at the agent's own completion. Named by the lead, since a lead is not a directory item. */
|
|
1026
998
|
| {
|
|
1027
999
|
type: "taken-over";
|
|
1028
1000
|
leadId: UserId;
|
|
@@ -1037,16 +1009,12 @@ export type TaskOutcome = {
|
|
|
1037
1009
|
| {
|
|
1038
1010
|
type: "expired";
|
|
1039
1011
|
phase: "pending" | "confirmed" | "preview";
|
|
1040
|
-
}
|
|
1041
|
-
/** This agent left a call that continues without them: a lead who joined and dropped. */
|
|
1042
|
-
| {
|
|
1043
|
-
type: "left";
|
|
1044
1012
|
} | {
|
|
1045
1013
|
type: "failed";
|
|
1046
1014
|
failure: ProtocolFailure;
|
|
1047
1015
|
};
|
|
1048
1016
|
export declare const TASK_COMMAND_NAMES: {
|
|
1049
|
-
readonly voice: readonly ["answer", "decline", "call", "hold", "resume", "end-call", "connect-back", "transfer", "lead-assist", "conference", "recording", "complete"];
|
|
1017
|
+
readonly voice: readonly ["answer", "decline", "call", "hold", "resume", "end-call", "terminate-call", "connect-back", "transfer", "lead-assist", "conference", "recording", "complete"];
|
|
1050
1018
|
readonly chat: readonly ["accept", "decline", "pause", "resume", "complete"];
|
|
1051
1019
|
readonly email: readonly ["accept", "decline", "complete"];
|
|
1052
1020
|
};
|
|
@@ -1069,10 +1037,14 @@ export type VoiceTaskCommand = {
|
|
|
1069
1037
|
} | {
|
|
1070
1038
|
type: "resume";
|
|
1071
1039
|
}
|
|
1072
|
-
/** End the
|
|
1040
|
+
/** End the agent's channel and every channel the agent added; the caller continues on the provider's path. Gated by `endCall`. */
|
|
1073
1041
|
| {
|
|
1074
1042
|
type: "end-call";
|
|
1075
1043
|
}
|
|
1044
|
+
/** End the caller channel. Gated by `terminateCall`. */
|
|
1045
|
+
| {
|
|
1046
|
+
type: "terminate-call";
|
|
1047
|
+
}
|
|
1076
1048
|
/** Issuable only in `completing`, under the `connectBack` capability. Dials the party's own number, so it names none. */
|
|
1077
1049
|
| {
|
|
1078
1050
|
type: "connect-back";
|
|
@@ -1113,16 +1085,6 @@ export type VoiceTaskCommand = {
|
|
|
1113
1085
|
type: "lead-assist";
|
|
1114
1086
|
action: "cancel";
|
|
1115
1087
|
}
|
|
1116
|
-
/** The lead keeps the customer; the agent's task ends `transferred`. Needs `Task.assisting`. */
|
|
1117
|
-
| {
|
|
1118
|
-
type: "lead-assist";
|
|
1119
|
-
action: "take-over-call";
|
|
1120
|
-
}
|
|
1121
|
-
/** The lead drops; the agent continues. The lead's task ends `left`. Needs `Task.assisting`. */
|
|
1122
|
-
| {
|
|
1123
|
-
type: "lead-assist";
|
|
1124
|
-
action: "leave";
|
|
1125
|
-
}
|
|
1126
1088
|
/** Dial the directory item `destinationId` into the call. Gated by `conference`. */
|
|
1127
1089
|
| {
|
|
1128
1090
|
type: "conference";
|
|
@@ -1230,12 +1192,6 @@ export declare const BREAK_KINDS: readonly ["short-break", "meal", "rest", "trai
|
|
|
1230
1192
|
* problem one level up. `other` matches nothing, including another provider's `other`.
|
|
1231
1193
|
*/
|
|
1232
1194
|
export type BreakKind = (typeof BREAK_KINDS)[number];
|
|
1233
|
-
/**
|
|
1234
|
-
* The breaks during which a lead may listen to a member's call: work of another sort, not rest.
|
|
1235
|
-
* A listening task is the one task a break in effect may hold, and only on one of these.
|
|
1236
|
-
*/
|
|
1237
|
-
export declare const LISTENING_BREAK_KINDS: readonly ["coaching", "administrative", "training"];
|
|
1238
|
-
export declare const breakKindAllowsListening: (kind: BreakKind | undefined) => boolean;
|
|
1239
1195
|
export interface BreakReason {
|
|
1240
1196
|
id: string;
|
|
1241
1197
|
label: string;
|
|
@@ -1309,24 +1265,65 @@ export interface TeamMember {
|
|
|
1309
1265
|
since?: IsoTimestamp;
|
|
1310
1266
|
/** A request in flight or a grant not yet in effect. `not-requested` is absence, and `on-break` is `availability: "on-break"`. */
|
|
1311
1267
|
break?: Extract<BreakStatus, "awaiting-approval" | "granted" | "starting-after-task">;
|
|
1268
|
+
/** The member's open tasks, trimmed, kept current by the provider; `[]` when they hold none, omitted only where the provider cannot see them. */
|
|
1269
|
+
tasks?: MemberTask[];
|
|
1270
|
+
/** Present while this lead is on the member's call, listening, coaching or joined; absent otherwise. */
|
|
1271
|
+
listening?: MemberListening;
|
|
1312
1272
|
}
|
|
1313
|
-
/** A member asking this lead to join their call. */
|
|
1273
|
+
/** A member asking this lead to join their call, named by the member and the assignment. */
|
|
1314
1274
|
export interface LeadRequest {
|
|
1315
|
-
id: string;
|
|
1316
1275
|
memberId: UserId;
|
|
1317
1276
|
assignmentId: AssignmentId;
|
|
1318
1277
|
note?: string;
|
|
1319
1278
|
since: IsoTimestamp;
|
|
1320
1279
|
}
|
|
1321
1280
|
/**
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
1281
|
+
* A member's task as the lead sees it: the same task the member's desk holds, trimmed by the
|
|
1282
|
+
* provider. The assignment, what the call is and where it stands, the room and the full history
|
|
1283
|
+
* are always here; the workspace -- browsers, controls, completion terms -- is the member's and
|
|
1284
|
+
* travels only if the provider chooses to send it.
|
|
1285
|
+
*/
|
|
1286
|
+
export type MemberTask<C extends Channel = Channel> = {
|
|
1287
|
+
assignmentId: AssignmentId;
|
|
1288
|
+
title: string;
|
|
1289
|
+
channel: C;
|
|
1290
|
+
taskType: string;
|
|
1291
|
+
phase: TaskPhase;
|
|
1292
|
+
party?: Contact;
|
|
1293
|
+
reference?: string;
|
|
1294
|
+
attributes?: TaskAttribute[];
|
|
1295
|
+
history?: TaskHistory;
|
|
1296
|
+
capabilities?: TaskCapabilities<C>;
|
|
1297
|
+
capabilitySource?: CapabilitySource;
|
|
1298
|
+
browsers?: TaskBrowser[];
|
|
1299
|
+
completionMode?: CompletionMode;
|
|
1300
|
+
wrapAllowance?: DurationSeconds;
|
|
1301
|
+
} & (C extends "voice" ? {
|
|
1302
|
+
onCall?: OnCall[];
|
|
1303
|
+
leadAssist?: TaskLeadAssist;
|
|
1304
|
+
takenOver?: TaskTakenOver;
|
|
1305
|
+
media?: TaskMediaState;
|
|
1306
|
+
} : {
|
|
1307
|
+
onCall?: never;
|
|
1308
|
+
leadAssist?: never;
|
|
1309
|
+
takenOver?: never;
|
|
1310
|
+
media?: never;
|
|
1311
|
+
});
|
|
1312
|
+
/** The lead on this member's call, in the mode they are heard, since when. */
|
|
1313
|
+
export interface MemberListening {
|
|
1314
|
+
mode: ListeningMode;
|
|
1315
|
+
since: IsoTimestamp;
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* The lead's view of their team: published to a login that declares `capabilities.lead` while the
|
|
1319
|
+
* lead has the team feature on, on every snapshot, and to nobody else. The provider keeps it
|
|
1320
|
+
* current: when a member holds, mutes or is transferred, the list is republished whole.
|
|
1324
1321
|
*/
|
|
1325
1322
|
export interface TeamMembers {
|
|
1326
1323
|
members: TeamMember[];
|
|
1327
|
-
/**
|
|
1328
|
-
requests
|
|
1329
|
-
/** The team's policy per capability, as it stands. Present
|
|
1324
|
+
/** Members asking a lead to join their call; `[]` when nobody is asking. */
|
|
1325
|
+
requests: LeadRequest[];
|
|
1326
|
+
/** The team's policy per capability, as it stands. Present where the provider offers policy control; absent where it does not. */
|
|
1330
1327
|
policies?: TeamPolicies;
|
|
1331
1328
|
}
|
|
1332
1329
|
/** A capability a team policy can name: any task control, new call, or a skill by its provider id. */
|
|
@@ -1339,42 +1336,57 @@ export interface TeamPolicy extends Resolved {
|
|
|
1339
1336
|
}
|
|
1340
1337
|
export type TeamPolicies = Partial<Record<PolicyKey, TeamPolicy>>;
|
|
1341
1338
|
export type TeamPolicyCommand = {
|
|
1342
|
-
type: "set";
|
|
1339
|
+
type: "set-policy";
|
|
1343
1340
|
capability: PolicyKey;
|
|
1344
1341
|
setting: TeamPolicySetting;
|
|
1345
1342
|
};
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1343
|
+
/**
|
|
1344
|
+
* The team feature on or off. The lead flag enables it; the lead switches it on the fly, and the
|
|
1345
|
+
* provider needs to know: while it is on, team events scoped to the lead's team arrive and the
|
|
1346
|
+
* lead may act; while it is off, the lead is an ordinary agent and nothing of the team reaches them.
|
|
1347
|
+
*/
|
|
1348
|
+
export type TeamFeatureCommand = {
|
|
1349
|
+
type: "lead-features";
|
|
1350
|
+
enabled: boolean;
|
|
1351
|
+
};
|
|
1352
|
+
/**
|
|
1353
|
+
* The lead's acts on a member's call, each naming the member and, where the lead has it, the
|
|
1354
|
+
* member's assignment; the provider resolves the member's current assignment from the member alone.
|
|
1355
|
+
* `join` answers a member's request and `decline` refuses it; `listen` starts unasked, silent,
|
|
1356
|
+
* `coach` is heard by the member alone and `join-call` by everyone; `leave` takes the lead's channel
|
|
1357
|
+
* off the call however it got there; `take-over-call` moves the call to the lead as its new agent,
|
|
1358
|
+
* from a joined call or a listened one alike. One member's call at a time.
|
|
1359
|
+
*/
|
|
1360
|
+
export type TeamCallCommand = {
|
|
1350
1361
|
type: "join";
|
|
1351
|
-
|
|
1362
|
+
memberId: UserId;
|
|
1363
|
+
assignmentId?: AssignmentId;
|
|
1352
1364
|
} | {
|
|
1353
1365
|
type: "decline";
|
|
1354
|
-
|
|
1366
|
+
memberId: UserId;
|
|
1367
|
+
assignmentId?: AssignmentId;
|
|
1355
1368
|
reason?: string;
|
|
1356
|
-
}
|
|
1357
|
-
export interface TeamLeadAssistCommandRequest {
|
|
1358
|
-
command: TeamLeadAssistCommand;
|
|
1359
|
-
}
|
|
1360
|
-
/**
|
|
1361
|
-
* A lead listening to a member's call. `listen` starts one, silent, on a member from the team member list;
|
|
1362
|
-
* `coach` and `join-call` change how the standing one is heard; `leave` ends it. One member at a
|
|
1363
|
-
* time, and only the modes the login's `listeningControl` lists.
|
|
1364
|
-
*/
|
|
1365
|
-
export type TeamListenCommand = {
|
|
1369
|
+
} | {
|
|
1366
1370
|
type: "listen";
|
|
1367
1371
|
memberId: UserId;
|
|
1372
|
+
assignmentId?: AssignmentId;
|
|
1368
1373
|
} | {
|
|
1369
1374
|
type: "coach";
|
|
1375
|
+
memberId: UserId;
|
|
1376
|
+
assignmentId?: AssignmentId;
|
|
1370
1377
|
} | {
|
|
1371
1378
|
type: "join-call";
|
|
1379
|
+
memberId: UserId;
|
|
1380
|
+
assignmentId?: AssignmentId;
|
|
1372
1381
|
} | {
|
|
1373
1382
|
type: "leave";
|
|
1383
|
+
memberId: UserId;
|
|
1384
|
+
assignmentId?: AssignmentId;
|
|
1385
|
+
} | {
|
|
1386
|
+
type: "take-over-call";
|
|
1387
|
+
memberId: UserId;
|
|
1388
|
+
assignmentId?: AssignmentId;
|
|
1374
1389
|
};
|
|
1375
|
-
export interface TeamListenCommandRequest {
|
|
1376
|
-
command: TeamListenCommand;
|
|
1377
|
-
}
|
|
1378
1390
|
export type TeamBreakCommand = {
|
|
1379
1391
|
type: "decide-break-request";
|
|
1380
1392
|
memberId: UserId;
|
|
@@ -1397,15 +1409,17 @@ export type TeamBreakCommand = {
|
|
|
1397
1409
|
type: "end-forced-break";
|
|
1398
1410
|
memberId: UserId;
|
|
1399
1411
|
};
|
|
1412
|
+
/** Every lead act, as `execute` takes a `TaskCommand`: discriminated by `type`. */
|
|
1413
|
+
export type TeamCommand = TeamFeatureCommand | TeamBreakCommand | TeamCallCommand | TeamPolicyCommand;
|
|
1414
|
+
export interface TeamCommandRequest {
|
|
1415
|
+
command: TeamCommand;
|
|
1416
|
+
}
|
|
1400
1417
|
export type TeamCommandResult = {
|
|
1401
1418
|
status: "applied";
|
|
1402
1419
|
} | {
|
|
1403
1420
|
status: "failed";
|
|
1404
1421
|
failure: ProtocolFailure;
|
|
1405
1422
|
};
|
|
1406
|
-
export interface TeamBreakCommandRequest {
|
|
1407
|
-
command: TeamBreakCommand;
|
|
1408
|
-
}
|
|
1409
1423
|
/**
|
|
1410
1424
|
* The task's real-time audio as the provider holds it: `started` while audio should be attached,
|
|
1411
1425
|
* `ended` once primary interaction's audio ended, and the field omitted while none should be. The
|
|
@@ -1574,6 +1588,20 @@ export type ProviderEvent<C extends Channel = Channel> = {
|
|
|
1574
1588
|
} | {
|
|
1575
1589
|
type: "task-media-ended";
|
|
1576
1590
|
assignmentId: AssignmentId;
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* The lead's channel is in a member's call -- listening, coaching or joined -- and its audio should
|
|
1594
|
+
* attach: on a softphone the application opens media on the member's assignment as it does for a
|
|
1595
|
+
* task. Voice, to a login that declares `lead`, never to the member.
|
|
1596
|
+
*/
|
|
1597
|
+
| {
|
|
1598
|
+
type: "team-media-started";
|
|
1599
|
+
memberId: UserId;
|
|
1600
|
+
assignmentId: AssignmentId;
|
|
1601
|
+
} | {
|
|
1602
|
+
type: "team-media-ended";
|
|
1603
|
+
memberId: UserId;
|
|
1604
|
+
assignmentId: AssignmentId;
|
|
1577
1605
|
} | {
|
|
1578
1606
|
type: "task-ended";
|
|
1579
1607
|
assignmentId: AssignmentId;
|
|
@@ -1687,14 +1715,8 @@ export interface Connection<C extends Channel = Channel> {
|
|
|
1687
1715
|
commitBreak?(): Promise<BreakCommitResult>;
|
|
1688
1716
|
cancelBreak?(): Promise<BreakCancelResult>;
|
|
1689
1717
|
endBreak?(): Promise<BreakEndResult>;
|
|
1690
|
-
/** Required when the login declares `capabilities.team.
|
|
1691
|
-
|
|
1692
|
-
/** Required when the login declares `capabilities.team.leadAssistControl`. */
|
|
1693
|
-
executeTeamLeadAssist?(request: TeamLeadAssistCommandRequest): Promise<TeamCommandResult>;
|
|
1694
|
-
/** Required when the login declares `capabilities.team.listeningControl`. */
|
|
1695
|
-
executeTeamListen?(request: TeamListenCommandRequest): Promise<TeamCommandResult>;
|
|
1696
|
-
/** Required when the login declares `capabilities.team.policyControl`. */
|
|
1697
|
-
executeTeamPolicy?(request: TeamPolicyCommandRequest): Promise<TeamCommandResult>;
|
|
1718
|
+
/** Required when the login declares `capabilities.lead`: every lead act, on the team surface and nowhere else. */
|
|
1719
|
+
executeTeam?(request: TeamCommandRequest): Promise<TeamCommandResult>;
|
|
1698
1720
|
/** Required of a voice adapter whose manifest lists `softphone`: on one, the call's audio lands in Omni. */
|
|
1699
1721
|
openMedia?(request: OpenMediaRequest): Promise<OpenMediaResult>;
|
|
1700
1722
|
/** Required when the login declares `capabilities.preferences`: the person's own choice, kept by the provider and republished as `authenticated`. */
|
|
@@ -1791,7 +1813,7 @@ export interface BrowserSessionKeyInput {
|
|
|
1791
1813
|
* Whether two logins declare the same capabilities, field by field. The comparison an adapter
|
|
1792
1814
|
* makes before republishing `authenticated`: capabilities are current, not fixed, and the natural
|
|
1793
1815
|
* guard -- comparing the identity -- never fires on a demotion, because the thing that changed is
|
|
1794
|
-
* not the thing being compared. Key order does not matter, and
|
|
1816
|
+
* not the thing being compared. Key order does not matter, and a withdrawn `lead` is a change.
|
|
1795
1817
|
*/
|
|
1796
1818
|
export declare function sameCapabilities(a: UserCapabilities, b: UserCapabilities): boolean;
|
|
1797
1819
|
/**
|
package/dist/index.js
CHANGED
|
@@ -76,7 +76,7 @@ export const CAPABILITY_SOURCES = ["queue", "ungoverned", "undetermined"];
|
|
|
76
76
|
// Task commands.
|
|
77
77
|
// ---------------------------------------------------------------------------
|
|
78
78
|
export const TASK_COMMAND_NAMES = {
|
|
79
|
-
voice: ["answer", "decline", "call", "hold", "resume", "end-call",
|
|
79
|
+
voice: ["answer", "decline", "call", "hold", "resume", "end-call", "terminate-call",
|
|
80
80
|
"connect-back", "transfer", "lead-assist", "conference", "recording", "complete"],
|
|
81
81
|
chat: ["accept", "decline", "pause", "resume", "complete"],
|
|
82
82
|
email: ["accept", "decline", "complete"],
|
|
@@ -95,12 +95,6 @@ export const BREAK_KINDS = [
|
|
|
95
95
|
"short-break", "meal", "rest", "training", "coaching",
|
|
96
96
|
"meeting", "administrative", "technical", "personal", "other",
|
|
97
97
|
];
|
|
98
|
-
/**
|
|
99
|
-
* The breaks during which a lead may listen to a member's call: work of another sort, not rest.
|
|
100
|
-
* A listening task is the one task a break in effect may hold, and only on one of these.
|
|
101
|
-
*/
|
|
102
|
-
export const LISTENING_BREAK_KINDS = ["coaching", "administrative", "training"];
|
|
103
|
-
export const breakKindAllowsListening = (kind) => kind !== undefined && LISTENING_BREAK_KINDS.includes(kind);
|
|
104
98
|
export const OMNI_FAILURE_CODES = [
|
|
105
99
|
"omni.not-authenticated",
|
|
106
100
|
"omni.capability-not-enabled",
|
|
@@ -177,19 +171,15 @@ export function historyStepExpectsAPerson(step) {
|
|
|
177
171
|
* Whether two logins declare the same capabilities, field by field. The comparison an adapter
|
|
178
172
|
* makes before republishing `authenticated`: capabilities are current, not fixed, and the natural
|
|
179
173
|
* guard -- comparing the identity -- never fires on a demotion, because the thing that changed is
|
|
180
|
-
* not the thing being compared. Key order does not matter, and
|
|
174
|
+
* not the thing being compared. Key order does not matter, and a withdrawn `lead` is a change.
|
|
181
175
|
*/
|
|
182
176
|
export function sameCapabilities(a, b) {
|
|
183
|
-
const modes = (team) => team?.listeningControl === undefined ? undefined : [...team.listeningControl].sort().join(",");
|
|
184
177
|
// Every field, so a capability added later cannot be missed here: `satisfies` pins the field
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
void { breaks: true, preferences: true, team: true };
|
|
178
|
+
// list to the type, and a new key is a compile error until it is compared.
|
|
179
|
+
void { breaks: true, preferences: true, lead: true };
|
|
188
180
|
const preferences = (list) => list === undefined ? undefined : list.map(p => [p.id, p.label, p.enabled, p.setBy, p.lockedBy ?? "", p.reason ?? ""].join("\u0000")).sort().join("\u0001");
|
|
189
181
|
return a.breaks === b.breaks &&
|
|
190
|
-
|
|
191
|
-
Object.keys(teamKeys).every(key => key === "listeningControl" || a.team?.[key] === b.team?.[key]) &&
|
|
192
|
-
modes(a.team) === modes(b.team) &&
|
|
182
|
+
a.lead === b.lead &&
|
|
193
183
|
preferences(a.preferences) === preferences(b.preferences);
|
|
194
184
|
}
|
|
195
185
|
/**
|
package/dist/testing.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation }
|
|
|
7
7
|
* rules -- each optional part of a task, each optional part of the break state and team member list, each
|
|
8
8
|
* declared contribution, and each event type.
|
|
9
9
|
*/
|
|
10
|
-
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.history", "task.onCall", "task.leadAssist", "task.
|
|
10
|
+
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.history", "task.onCall", "task.leadAssist", "task.takenOver", "task.media", "task.acceptance", "task.outcomes", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.forced", "team.members", "team.requests", "team.tasks", "team.listening", "contacts", "scheduledActivities", "team.policies"];
|
|
11
11
|
export type ContractSubject = (typeof STATE_SUBJECTS)[number] | `event.${ProviderEvent["type"]}`;
|
|
12
12
|
export interface AdapterContractResult {
|
|
13
13
|
events: ProviderEventEnvelope[];
|
package/dist/testing.js
CHANGED
|
@@ -14,8 +14,7 @@ const STATE_SUBJECTS = [
|
|
|
14
14
|
"task.history",
|
|
15
15
|
"task.onCall",
|
|
16
16
|
"task.leadAssist",
|
|
17
|
-
"task.
|
|
18
|
-
"task.listening",
|
|
17
|
+
"task.takenOver",
|
|
19
18
|
"task.media",
|
|
20
19
|
"task.acceptance",
|
|
21
20
|
"task.outcomes",
|
|
@@ -26,6 +25,8 @@ const STATE_SUBJECTS = [
|
|
|
26
25
|
"break.forced",
|
|
27
26
|
"team.members",
|
|
28
27
|
"team.requests",
|
|
28
|
+
"team.tasks",
|
|
29
|
+
"team.listening",
|
|
29
30
|
"contacts",
|
|
30
31
|
"scheduledActivities",
|
|
31
32
|
"team.policies",
|
|
@@ -35,7 +36,7 @@ const STATE_SUBJECTS = [
|
|
|
35
36
|
const EVENT_TYPES = {
|
|
36
37
|
snapshot: true, "transport-status": true, "break-state": true, "task-offered": true, "task-updated": true,
|
|
37
38
|
"task-media-started": true, "task-media-ended": true, "task-ended": true, "dial-outcome": true, announcement: true, "queue-summary": true, diagnostic: true,
|
|
38
|
-
"team-updated": true, "contacts-updated": true, "calendar-updated": true,
|
|
39
|
+
"team-updated": true, "team-media-started": true, "team-media-ended": true, "contacts-updated": true, "calendar-updated": true,
|
|
39
40
|
};
|
|
40
41
|
const CONTRACT_SUBJECTS = [
|
|
41
42
|
...STATE_SUBJECTS,
|
|
@@ -59,10 +60,8 @@ function observeTask(value, seen) {
|
|
|
59
60
|
seen.add("task.onCall");
|
|
60
61
|
if (value.leadAssist !== undefined)
|
|
61
62
|
seen.add("task.leadAssist");
|
|
62
|
-
if (value.
|
|
63
|
-
seen.add("task.
|
|
64
|
-
if (value.listening !== undefined)
|
|
65
|
-
seen.add("task.listening");
|
|
63
|
+
if (value.takenOver !== undefined)
|
|
64
|
+
seen.add("task.takenOver");
|
|
66
65
|
if (value.media !== undefined)
|
|
67
66
|
seen.add("task.media");
|
|
68
67
|
if (value.acceptance !== undefined)
|
|
@@ -95,6 +94,15 @@ function observeTeam(value, seen) {
|
|
|
95
94
|
seen.add("team.members");
|
|
96
95
|
if (some(value.requests))
|
|
97
96
|
seen.add("team.requests");
|
|
97
|
+
if (Array.isArray(value.members))
|
|
98
|
+
for (const member of value.members) {
|
|
99
|
+
if (!isRecord(member))
|
|
100
|
+
continue;
|
|
101
|
+
if (some(member.tasks))
|
|
102
|
+
seen.add("team.tasks");
|
|
103
|
+
if (member.listening !== undefined)
|
|
104
|
+
seen.add("team.listening");
|
|
105
|
+
}
|
|
98
106
|
if (isRecord(value.policies) && Object.keys(value.policies).length > 0)
|
|
99
107
|
seen.add("team.policies");
|
|
100
108
|
}
|
|
@@ -229,14 +237,8 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
229
237
|
requireMethod(on, method, "the login declares capabilities.breaks");
|
|
230
238
|
}
|
|
231
239
|
}
|
|
232
|
-
if (capabilities.
|
|
233
|
-
requireMethod(on, "
|
|
234
|
-
if (capabilities.team?.leadAssistControl === true)
|
|
235
|
-
requireMethod(on, "executeTeamLeadAssist", "the login declares capabilities.team.leadAssistControl");
|
|
236
|
-
if (capabilities.team?.listeningControl !== undefined)
|
|
237
|
-
requireMethod(on, "executeTeamListen", "the login declares capabilities.team.listeningControl");
|
|
238
|
-
if (capabilities.team?.policyControl === true)
|
|
239
|
-
requireMethod(on, "executeTeamPolicy", "the login declares capabilities.team.policyControl");
|
|
240
|
+
if (capabilities.lead === true)
|
|
241
|
+
requireMethod(on, "executeTeam", "the login declares capabilities.lead");
|
|
240
242
|
if (some(capabilities.preferences))
|
|
241
243
|
requireMethod(on, "setPreference", "the login declares capabilities.preferences");
|
|
242
244
|
};
|
|
@@ -671,11 +673,28 @@ function userIdsIn(snapshot) {
|
|
|
671
673
|
add(entry.userId);
|
|
672
674
|
if (isRecord(t.leadAssist))
|
|
673
675
|
add(t.leadAssist.leadId);
|
|
674
|
-
if (isRecord(t.
|
|
675
|
-
add(t.
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
676
|
+
if (isRecord(t.takenOver))
|
|
677
|
+
add(t.takenOver.memberId);
|
|
678
|
+
}
|
|
679
|
+
if (isRecord(team)) {
|
|
680
|
+
for (const member of Array.isArray(team.members) ? team.members : []) {
|
|
681
|
+
if (!isRecord(member) || !Array.isArray(member.tasks))
|
|
682
|
+
continue;
|
|
683
|
+
for (const task of member.tasks) {
|
|
684
|
+
const t = task;
|
|
685
|
+
if (isRecord(t.history) && Array.isArray(t.history.steps))
|
|
686
|
+
for (const step of t.history.steps)
|
|
687
|
+
if (isRecord(step))
|
|
688
|
+
add(step.by);
|
|
689
|
+
if (Array.isArray(t.onCall))
|
|
690
|
+
for (const entry of t.onCall)
|
|
691
|
+
if (isRecord(entry))
|
|
692
|
+
add(entry.userId);
|
|
693
|
+
if (isRecord(t.leadAssist))
|
|
694
|
+
add(t.leadAssist.leadId);
|
|
695
|
+
if (isRecord(t.takenOver))
|
|
696
|
+
add(t.takenOver.memberId);
|
|
697
|
+
}
|
|
679
698
|
}
|
|
680
699
|
}
|
|
681
700
|
return [...ids];
|
|
@@ -683,8 +702,7 @@ function userIdsIn(snapshot) {
|
|
|
683
702
|
const teamNamesUsers = (team) => isRecord(team) && (some(team.members) || some(team.requests));
|
|
684
703
|
const taskNamesUsers = (task) => isRecord(task) && ((isRecord(task.history) && Array.isArray(task.history.steps) && task.history.steps.some(step => isRecord(step) && step.by !== undefined)) ||
|
|
685
704
|
(isRecord(task.leadAssist) && task.leadAssist.leadId !== undefined) ||
|
|
686
|
-
isRecord(task.
|
|
687
|
-
isRecord(task.listening));
|
|
705
|
+
isRecord(task.takenOver));
|
|
688
706
|
/** Whether an event publishes a `UserId`, on a team member list, a task, or the snapshot a reconnect carries. */
|
|
689
707
|
function eventNamesUsers(envelope) {
|
|
690
708
|
const event = isRecord(envelope) ? envelope.event : undefined;
|
|
@@ -815,11 +833,7 @@ export function assertCapabilityWithdrawal(states, snapshot, manifest) {
|
|
|
815
833
|
const before = first.capabilities;
|
|
816
834
|
const after = last.capabilities;
|
|
817
835
|
const withdrawn = (before.breaks === true && after.breaks !== true) ||
|
|
818
|
-
(before.
|
|
819
|
-
(before.team?.breakControl === true && after.team?.breakControl !== true) ||
|
|
820
|
-
(before.team?.leadAssistControl === true && after.team?.leadAssistControl !== true) ||
|
|
821
|
-
(before.team?.listeningControl !== undefined && after.team?.listeningControl === undefined) ||
|
|
822
|
-
(before.team?.policyControl === true && after.team?.policyControl !== true) ||
|
|
836
|
+
(before.lead === true && after.lead !== true) ||
|
|
823
837
|
(before.preferences ?? []).some(was => !(after.preferences ?? []).some(now => now.id === was.id));
|
|
824
838
|
if (!withdrawn) {
|
|
825
839
|
throw new Error("Capability withdrawal must end with at least one capability the first login declared withdrawn");
|
package/dist/validation.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RecordingState, type ProtocolViolation, type UserCapabilities, type UserId } from "./index.js";
|
|
1
|
+
import { type RecordingState, type Channel, type ProtocolViolation, type UserCapabilities, type UserId } from "./index.js";
|
|
2
2
|
export type { ProtocolViolation } from "./index.js";
|
|
3
3
|
export declare class ProtocolConformanceError extends Error {
|
|
4
4
|
readonly violations: readonly ProtocolViolation[];
|
|
@@ -45,16 +45,23 @@ export interface TaskValidationContext {
|
|
|
45
45
|
* carry one of these. Unknown to a host, which never sees the value, and then unchecked.
|
|
46
46
|
*/
|
|
47
47
|
locked?: readonly string[];
|
|
48
|
+
/**
|
|
49
|
+
* A member's task on the team member list, trimmed by the provider: the workspace -- controls,
|
|
50
|
+
* their source, browsers, completion terms -- is the member's and may be absent; what is present
|
|
51
|
+
* is held to the same rules as on the member's own desk.
|
|
52
|
+
*/
|
|
53
|
+
member?: true;
|
|
48
54
|
}
|
|
49
55
|
export declare function validateTask(task: unknown, context: TaskValidationContext, path?: string): ProtocolViolation[];
|
|
50
56
|
/** Checks break status against the full currently retained task set, after a transaction. */
|
|
51
57
|
export declare function validateBreakStatus(state: unknown, tasks: unknown, path?: string): ProtocolViolation[];
|
|
52
58
|
/**
|
|
53
|
-
* Checks lead
|
|
54
|
-
* for force-break
|
|
55
|
-
*
|
|
59
|
+
* Checks a lead command before dispatch, against the current authentication, transport and team
|
|
60
|
+
* member list (`team`) and, for force-break and end-forced-break, the target's full `memberBreak`.
|
|
61
|
+
* The authority is the login's `lead` flag; the provider rechecks the target and the state
|
|
62
|
+
* atomically, since a team member list is not authority to act after it has changed.
|
|
56
63
|
*/
|
|
57
|
-
export declare function
|
|
64
|
+
export declare function validateTeamCommand(request: unknown, context: unknown, path?: string): ProtocolViolation[];
|
|
58
65
|
/** The four agent break methods; this is a validation API, not a wire command. */
|
|
59
66
|
export type BreakMethod = "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak";
|
|
60
67
|
/**
|
|
@@ -90,6 +97,13 @@ export interface ReaderContext {
|
|
|
90
97
|
levels?: readonly string[];
|
|
91
98
|
/** The login's `loginId`. A snapshot or event naming another belongs to a login that is gone. */
|
|
92
99
|
loginId?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Whether the lead has the team feature on, as the application last told the provider. On by
|
|
102
|
+
* default for a lead; off, no team member list is owed or expected. Unknown to a caller without it.
|
|
103
|
+
*/
|
|
104
|
+
leadFeatures?: boolean;
|
|
105
|
+
/** The provider's channel, for a member's task that states none. Filled from the manifest by `validateSnapshot` and `validateEventEnvelope`. */
|
|
106
|
+
channel?: Channel;
|
|
93
107
|
/**
|
|
94
108
|
* The values the queue locked on this login's tasks, as whoever runs the validator knows them:
|
|
95
109
|
* a conformance run states them, a host never has them. Where a task's party stands locked, no
|
|
@@ -135,7 +149,7 @@ export declare function validateTaskCommandRequest(request: unknown, task: unkno
|
|
|
135
149
|
*/
|
|
136
150
|
export declare function validateTaskCommand(command: unknown, task?: unknown, path?: string, taskContext?: Omit<TaskValidationContext, "channel">): ProtocolViolation[];
|
|
137
151
|
/** The connection methods whose results `validateResult` knows. */
|
|
138
|
-
export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "
|
|
152
|
+
export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeam" | "openMedia" | "setPreference" | "recordStep";
|
|
139
153
|
/**
|
|
140
154
|
* Validates what a connection method answered. A result is untrusted for the same reason a
|
|
141
155
|
* snapshot is: it comes from an adapter that may be compiled against another version, and Omni
|