@xema/omni-protocol 0.1.40 → 0.1.42
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 +53 -0
- package/dist/index.js +10 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +9 -2
- package/dist/validation.d.ts +1 -1
- package/dist/validation.js +73 -5
- package/guide.md +95 -3
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -175,9 +175,21 @@ export interface TeamCapabilities {
|
|
|
175
175
|
breakControl?: true;
|
|
176
176
|
/** This lead may join a member's call on request. Requires `executeTeamLeadAssist`. */
|
|
177
177
|
leadAssistControl?: true;
|
|
178
|
+
/**
|
|
179
|
+
* This lead may listen to a member's call unasked, in these modes and no others. Whisper and
|
|
180
|
+
* barge begin from a monitor, so the list always includes `monitor`. Nothing of it reaches the
|
|
181
|
+
* member's task. Requires `executeTeamMonitor`.
|
|
182
|
+
*/
|
|
183
|
+
monitorControl?: MonitorMode[];
|
|
178
184
|
/** This lead sets the team's policy per capability -- on, off, or the agent's -- within what the queue allows. Requires `executeTeamPolicy`. */
|
|
179
185
|
policyControl?: true;
|
|
180
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* How a lead listening to a member's call is heard: `monitor` hears both sides in silence,
|
|
189
|
+
* `whisper` is heard by the agent alone, `barge` by everyone on the call.
|
|
190
|
+
*/
|
|
191
|
+
export type MonitorMode = "monitor" | "whisper" | "barge";
|
|
192
|
+
export declare const MONITOR_MODES: readonly ["monitor", "whisper", "barge"];
|
|
181
193
|
/**
|
|
182
194
|
* What this login may do, beyond any one task. It travels with the identity because it is part
|
|
183
195
|
* of who the agent is on this provider: the provider knows the roles and says so at sign-in.
|
|
@@ -623,6 +635,19 @@ export interface TaskAssisting {
|
|
|
623
635
|
note?: string;
|
|
624
636
|
since: IsoTimestamp;
|
|
625
637
|
}
|
|
638
|
+
/**
|
|
639
|
+
* On the lead's own task while they listen to a member's call: whose call, which call, and how the
|
|
640
|
+
* lead is heard, restated on every change of mode. Nothing of it reaches the member's task. There
|
|
641
|
+
* is no take-over here; a lead who wants the call uses lead assist. It is the lead's only task:
|
|
642
|
+
* a lead listens while holding no work of their own, idle or on a break of a kind in
|
|
643
|
+
* `MONITORING_BREAK_KINDS`.
|
|
644
|
+
*/
|
|
645
|
+
export interface TaskMonitoring {
|
|
646
|
+
memberId: UserId;
|
|
647
|
+
taskId: TaskId;
|
|
648
|
+
mode: MonitorMode;
|
|
649
|
+
since: IsoTimestamp;
|
|
650
|
+
}
|
|
626
651
|
/**
|
|
627
652
|
* A level of the organisation's structure, by the id its manifest declares -- or one of the four
|
|
628
653
|
* a typical organisation has, `DEFAULT_LEVELS`, when the manifest declares no ladder. The protocol
|
|
@@ -693,11 +718,13 @@ export type Task<C extends Channel = Channel> = {
|
|
|
693
718
|
onCall?: OnCall[];
|
|
694
719
|
leadAssist?: TaskLeadAssist;
|
|
695
720
|
assisting?: TaskAssisting;
|
|
721
|
+
monitoring?: TaskMonitoring;
|
|
696
722
|
media?: TaskMediaState;
|
|
697
723
|
} : {
|
|
698
724
|
onCall?: never;
|
|
699
725
|
leadAssist?: never;
|
|
700
726
|
assisting?: never;
|
|
727
|
+
monitoring?: never;
|
|
701
728
|
media?: never;
|
|
702
729
|
});
|
|
703
730
|
/**
|
|
@@ -902,6 +929,12 @@ export declare const BREAK_KINDS: readonly ["short-break", "meal", "rest", "trai
|
|
|
902
929
|
* problem one level up. `other` matches nothing, including another provider's `other`.
|
|
903
930
|
*/
|
|
904
931
|
export type BreakKind = (typeof BREAK_KINDS)[number];
|
|
932
|
+
/**
|
|
933
|
+
* The breaks during which a lead may listen to a member's call: work of another sort, not rest.
|
|
934
|
+
* A monitoring task is the one task a break in effect may hold, and only on one of these.
|
|
935
|
+
*/
|
|
936
|
+
export declare const MONITORING_BREAK_KINDS: readonly ["coaching", "administrative", "training"];
|
|
937
|
+
export declare const breakKindAllowsMonitoring: (kind: BreakKind | undefined) => boolean;
|
|
905
938
|
export interface BreakReason {
|
|
906
939
|
id: string;
|
|
907
940
|
label: string;
|
|
@@ -1032,6 +1065,24 @@ export type TeamLeadAssistCommand = {
|
|
|
1032
1065
|
export interface TeamLeadAssistCommandRequest {
|
|
1033
1066
|
command: TeamLeadAssistCommand;
|
|
1034
1067
|
}
|
|
1068
|
+
/**
|
|
1069
|
+
* A lead listening to a member's call. `monitor` starts one, silent, on a member from the roster;
|
|
1070
|
+
* `whisper` and `barge` change how the standing one is heard; `leave` ends it. One member at a
|
|
1071
|
+
* time, and only the modes the login's `monitorControl` lists.
|
|
1072
|
+
*/
|
|
1073
|
+
export type TeamMonitorCommand = {
|
|
1074
|
+
type: "monitor";
|
|
1075
|
+
memberId: UserId;
|
|
1076
|
+
} | {
|
|
1077
|
+
type: "whisper";
|
|
1078
|
+
} | {
|
|
1079
|
+
type: "barge";
|
|
1080
|
+
} | {
|
|
1081
|
+
type: "leave";
|
|
1082
|
+
};
|
|
1083
|
+
export interface TeamMonitorCommandRequest {
|
|
1084
|
+
command: TeamMonitorCommand;
|
|
1085
|
+
}
|
|
1035
1086
|
export type TeamBreakCommand = {
|
|
1036
1087
|
type: "decide";
|
|
1037
1088
|
memberId: UserId;
|
|
@@ -1296,6 +1347,8 @@ export interface Connection<C extends Channel = Channel> {
|
|
|
1296
1347
|
executeTeamBreak?(request: TeamBreakCommandRequest): Promise<TeamCommandResult>;
|
|
1297
1348
|
/** Required when the login declares `capabilities.team.leadAssistControl`. */
|
|
1298
1349
|
executeTeamLeadAssist?(request: TeamLeadAssistCommandRequest): Promise<TeamCommandResult>;
|
|
1350
|
+
/** Required when the login declares `capabilities.team.monitorControl`. */
|
|
1351
|
+
executeTeamMonitor?(request: TeamMonitorCommandRequest): Promise<TeamCommandResult>;
|
|
1299
1352
|
/** Required when the login declares `capabilities.team.policyControl`. */
|
|
1300
1353
|
executeTeamPolicy?(request: TeamPolicyCommandRequest): Promise<TeamCommandResult>;
|
|
1301
1354
|
/** Required of every voice adapter: all voice audio lands in Omni. */
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ export const IDLE_CAPABILITY_UI = {
|
|
|
25
25
|
calendar: "Calendar",
|
|
26
26
|
contacts: "Contacts",
|
|
27
27
|
};
|
|
28
|
+
export const MONITOR_MODES = ["monitor", "whisper", "barge"];
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
// Task workspace.
|
|
30
31
|
// ---------------------------------------------------------------------------
|
|
@@ -89,6 +90,12 @@ export const BREAK_KINDS = [
|
|
|
89
90
|
"short-break", "meal", "rest", "training", "coaching",
|
|
90
91
|
"meeting", "administrative", "technical", "personal", "other",
|
|
91
92
|
];
|
|
93
|
+
/**
|
|
94
|
+
* The breaks during which a lead may listen to a member's call: work of another sort, not rest.
|
|
95
|
+
* A monitoring task is the one task a break in effect may hold, and only on one of these.
|
|
96
|
+
*/
|
|
97
|
+
export const MONITORING_BREAK_KINDS = ["coaching", "administrative", "training"];
|
|
98
|
+
export const breakKindAllowsMonitoring = (kind) => kind !== undefined && MONITORING_BREAK_KINDS.includes(kind);
|
|
92
99
|
export const OMNI_FAILURE_CODES = [
|
|
93
100
|
"omni.not-authenticated",
|
|
94
101
|
"omni.capability-not-enabled",
|
|
@@ -167,10 +174,12 @@ export function handlingStepExpectsAPerson(step) {
|
|
|
167
174
|
* not the thing being compared. Key order does not matter, and `team: {}` is not `team` absent.
|
|
168
175
|
*/
|
|
169
176
|
export function sameCapabilities(a, b) {
|
|
177
|
+
const modes = (team) => team?.monitorControl === undefined ? undefined : [...team.monitorControl].sort().join(",");
|
|
170
178
|
return a.breaks === b.breaks &&
|
|
171
179
|
(a.team === undefined) === (b.team === undefined) &&
|
|
172
180
|
a.team?.breakControl === b.team?.breakControl &&
|
|
173
|
-
a.team?.leadAssistControl === b.team?.leadAssistControl
|
|
181
|
+
a.team?.leadAssistControl === b.team?.leadAssistControl &&
|
|
182
|
+
modes(a.team) === modes(b.team);
|
|
174
183
|
}
|
|
175
184
|
/**
|
|
176
185
|
* The storage-profile key a reusing browser shares, or `undefined` where it shares nothing.
|
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 roster, each
|
|
8
8
|
* declared contribution, and each event type.
|
|
9
9
|
*/
|
|
10
|
-
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.handlingHistory", "task.onCall", "task.leadAssist", "task.assisting", "task.media", "task.acceptance", "task.dispositions", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.imposed", "team.members", "team.requests", "contacts", "scheduledActivities", "team.policies"];
|
|
10
|
+
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.handlingHistory", "task.onCall", "task.leadAssist", "task.assisting", "task.monitoring", "task.media", "task.acceptance", "task.dispositions", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.imposed", "team.members", "team.requests", "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
|
@@ -15,6 +15,7 @@ const STATE_SUBJECTS = [
|
|
|
15
15
|
"task.onCall",
|
|
16
16
|
"task.leadAssist",
|
|
17
17
|
"task.assisting",
|
|
18
|
+
"task.monitoring",
|
|
18
19
|
"task.media",
|
|
19
20
|
"task.acceptance",
|
|
20
21
|
"task.dispositions",
|
|
@@ -60,6 +61,8 @@ function observeTask(value, seen) {
|
|
|
60
61
|
seen.add("task.leadAssist");
|
|
61
62
|
if (value.assisting !== undefined)
|
|
62
63
|
seen.add("task.assisting");
|
|
64
|
+
if (value.monitoring !== undefined)
|
|
65
|
+
seen.add("task.monitoring");
|
|
63
66
|
if (value.media !== undefined)
|
|
64
67
|
seen.add("task.media");
|
|
65
68
|
if (value.acceptance !== undefined)
|
|
@@ -225,6 +228,8 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
225
228
|
requireMethod(on, "executeTeamBreak", "the login declares capabilities.team.breakControl");
|
|
226
229
|
if (capabilities.team?.leadAssistControl === true)
|
|
227
230
|
requireMethod(on, "executeTeamLeadAssist", "the login declares capabilities.team.leadAssistControl");
|
|
231
|
+
if (capabilities.team?.monitorControl !== undefined)
|
|
232
|
+
requireMethod(on, "executeTeamMonitor", "the login declares capabilities.team.monitorControl");
|
|
228
233
|
if (capabilities.team?.policyControl === true)
|
|
229
234
|
requireMethod(on, "executeTeamPolicy", "the login declares capabilities.team.policyControl");
|
|
230
235
|
if (some(capabilities.preferences))
|
|
@@ -405,7 +410,8 @@ function publishesUserIds(snapshot) {
|
|
|
405
410
|
const teamNamesUsers = (team) => isRecord(team) && (some(team.members) || some(team.requests));
|
|
406
411
|
const taskNamesUsers = (task) => isRecord(task) && ((isRecord(task.handlingHistory) && Array.isArray(task.handlingHistory.steps) && task.handlingHistory.steps.some(step => isRecord(step) && step.by !== undefined)) ||
|
|
407
412
|
(isRecord(task.leadAssist) && task.leadAssist.leadId !== undefined) ||
|
|
408
|
-
isRecord(task.assisting)
|
|
413
|
+
isRecord(task.assisting) ||
|
|
414
|
+
isRecord(task.monitoring));
|
|
409
415
|
/** Mute is the leg the host performs, so a task that allows it obliges the provider to take the record. */
|
|
410
416
|
const taskDeclaresMute = (task) => isRecord(task) && isRecord(task.capabilities) && task.capabilities.mute !== undefined;
|
|
411
417
|
function eventDeclaresMute(envelope) {
|
|
@@ -517,7 +523,8 @@ export function assertCapabilityWithdrawal(states, snapshot, manifest) {
|
|
|
517
523
|
const withdrawn = (before.breaks === true && after.breaks !== true) ||
|
|
518
524
|
(before.team !== undefined && after.team === undefined) ||
|
|
519
525
|
(before.team?.breakControl === true && after.team?.breakControl !== true) ||
|
|
520
|
-
(before.team?.leadAssistControl === true && after.team?.leadAssistControl !== true)
|
|
526
|
+
(before.team?.leadAssistControl === true && after.team?.leadAssistControl !== true) ||
|
|
527
|
+
(before.team?.monitorControl !== undefined && after.team?.monitorControl === undefined);
|
|
521
528
|
if (!withdrawn) {
|
|
522
529
|
throw new Error("Capability withdrawal must end with at least one capability the first login declared withdrawn");
|
|
523
530
|
}
|
package/dist/validation.d.ts
CHANGED
|
@@ -59,7 +59,7 @@ export declare function validateHostGuarantees(guarantees: unknown, path?: strin
|
|
|
59
59
|
export declare function validateHandlingReport(report: unknown, path?: string, manifest?: unknown): ProtocolViolation[];
|
|
60
60
|
export declare function validateHostReport(report: unknown, path?: string): ProtocolViolation[];
|
|
61
61
|
/** The connection methods whose results `validateResult` knows. */
|
|
62
|
-
export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeamBreak" | "executeTeamLeadAssist" | "openMedia" | "setPreference" | "recordStep" | "executeTeamPolicy";
|
|
62
|
+
export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeamBreak" | "executeTeamLeadAssist" | "executeTeamMonitor" | "openMedia" | "setPreference" | "recordStep" | "executeTeamPolicy";
|
|
63
63
|
/**
|
|
64
64
|
* Validates what a connection method answered. A result is untrusted for the same reason a
|
|
65
65
|
* snapshot is: it comes from an adapter that may be compiled against another version, and Omni
|
package/dist/validation.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Each list is pinned to its type both ways -- a member the type lacks, or a member the list
|
|
10
10
|
// lacks, fails to compile -- so what the validators accept cannot drift from what the
|
|
11
11
|
// declarations say.
|
|
12
|
-
import { ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, BROWSER_ISOLATION_SCHEMES, HANDLING_STEPS_THAT_DIAL, IDLE_CAPABILITIES, OMNI_FAILURE_CODES, OMNI_SUPPORTED_PROTOCOL_VERSIONS, negotiateProtocolVersion, DEFAULT_LEVELS, effectiveLevels, } from "./index.js";
|
|
12
|
+
import { ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, MONITORING_BREAK_KINDS, BROWSER_ISOLATION_SCHEMES, HANDLING_STEPS_THAT_DIAL, IDLE_CAPABILITIES, OMNI_FAILURE_CODES, OMNI_SUPPORTED_PROTOCOL_VERSIONS, negotiateProtocolVersion, DEFAULT_LEVELS, effectiveLevels, } from "./index.js";
|
|
13
13
|
export class ProtocolConformanceError extends Error {
|
|
14
14
|
violations;
|
|
15
15
|
constructor(violations, summary = "Adapter violates the Omni protocol") {
|
|
@@ -84,7 +84,8 @@ const MEMBER_BREAKS = membersOf({
|
|
|
84
84
|
const OFFERABLE_PHASES = membersOf({
|
|
85
85
|
pending: true, confirmed: true, preparing: true,
|
|
86
86
|
});
|
|
87
|
-
const TEAM_CAPABILITIES = membersOf({ breakControl: true, leadAssistControl: true, policyControl: true });
|
|
87
|
+
const TEAM_CAPABILITIES = membersOf({ breakControl: true, leadAssistControl: true, policyControl: true, monitorControl: true });
|
|
88
|
+
const MONITOR_MODES = membersOf({ monitor: true, whisper: true, barge: true });
|
|
88
89
|
const COMPLETED_BY = membersOf({ agent: true, provider: true });
|
|
89
90
|
const EXPIRABLE_PHASES = membersOf({
|
|
90
91
|
pending: true, confirmed: true, preparing: true,
|
|
@@ -835,6 +836,21 @@ function validateAssisting(value, channel, path, into) {
|
|
|
835
836
|
into.filled(value.note, "task.assisting.note", `${path}.note`, "a note must not be empty when present");
|
|
836
837
|
into.timestamp(value.since, "task.assisting.since", `${path}.since`);
|
|
837
838
|
}
|
|
839
|
+
/** The lead's own task while they listen: whose call, which call, and how they are heard. Voice only. */
|
|
840
|
+
function validateMonitoring(value, channel, path, into) {
|
|
841
|
+
if (value === undefined)
|
|
842
|
+
return;
|
|
843
|
+
if (!into.require(channel === "voice", "task.monitoring.channel", path, `a ${channel} task has no call to listen to`))
|
|
844
|
+
return;
|
|
845
|
+
if (!isPlainObject(value)) {
|
|
846
|
+
into.add("task.monitoring.shape", path, "monitoring must be an object when present");
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
into.require(isUserId(value.memberId), "task.monitoring.memberId", `${path}.memberId`, "a monitored call names the member on it");
|
|
850
|
+
into.require(isTaskId(value.taskId), "task.monitoring.taskId", `${path}.taskId`, "a monitored call names the member's task");
|
|
851
|
+
into.oneOf(value.mode, MONITOR_MODES, "task.monitoring.mode", `${path}.mode`);
|
|
852
|
+
into.timestamp(value.since, "task.monitoring.since", `${path}.since`);
|
|
853
|
+
}
|
|
838
854
|
export function validateTask(task, context, path = "task") {
|
|
839
855
|
const into = new Collector();
|
|
840
856
|
validateTaskInto(task, context, path, into);
|
|
@@ -888,6 +904,11 @@ function validateTaskInto(task, context, path, into) {
|
|
|
888
904
|
validateTaskMedia(task.media, context.channel, `${path}.media`, into);
|
|
889
905
|
validateLeadAssist(task.leadAssist, context.channel, `${path}.leadAssist`, into);
|
|
890
906
|
validateAssisting(task.assisting, context.channel, `${path}.assisting`, into);
|
|
907
|
+
validateMonitoring(task.monitoring, context.channel, `${path}.monitoring`, into);
|
|
908
|
+
// A lead's task is one thing: a call they joined on request, or a call they listen to unasked.
|
|
909
|
+
if (task.assisting !== undefined && task.monitoring !== undefined) {
|
|
910
|
+
into.add("task.monitoring.assisting", `${path}.monitoring`, "a task is a joined call or a monitored one, never both");
|
|
911
|
+
}
|
|
891
912
|
const capabilities = task.capabilities;
|
|
892
913
|
if (!isPlainObject(capabilities)) {
|
|
893
914
|
into.add("task.capabilities.shape", `${path}.capabilities`, "a task needs a capabilities object");
|
|
@@ -1146,14 +1167,20 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot", context
|
|
|
1146
1167
|
else {
|
|
1147
1168
|
const seen = new Set();
|
|
1148
1169
|
let assisting;
|
|
1170
|
+
let monitoring;
|
|
1149
1171
|
snapshot.tasks.forEach((task, index) => {
|
|
1150
1172
|
validateTaskInto(task, { channel, levels, autoAcceptTasks: context.autoAcceptTasks, dialOutcomesDeclared: manifestDials(manifest) }, `${path}.tasks[${index}]`, into);
|
|
1151
|
-
// A lead assists one call at a time.
|
|
1173
|
+
// A lead assists one call at a time, and listens to one at a time.
|
|
1152
1174
|
if (isPlainObject(task) && task.assisting !== undefined) {
|
|
1153
1175
|
if (assisting !== undefined)
|
|
1154
1176
|
into.add("snapshot.assisting.single", `${path}.tasks[${index}].assisting`, "a lead assists one call at a time");
|
|
1155
1177
|
assisting = index;
|
|
1156
1178
|
}
|
|
1179
|
+
if (isPlainObject(task) && task.monitoring !== undefined) {
|
|
1180
|
+
if (monitoring !== undefined)
|
|
1181
|
+
into.add("snapshot.monitoring.single", `${path}.tasks[${index}].monitoring`, "a lead listens to one call at a time");
|
|
1182
|
+
monitoring = index;
|
|
1183
|
+
}
|
|
1157
1184
|
if (isPlainObject(task) && isTaskId(task.id)) {
|
|
1158
1185
|
if (seen.has(task.id))
|
|
1159
1186
|
into.add("task.id.unique", `${path}.tasks[${index}].id`, `duplicate task id: ${task.id}`);
|
|
@@ -1161,11 +1188,31 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot", context
|
|
|
1161
1188
|
}
|
|
1162
1189
|
});
|
|
1163
1190
|
}
|
|
1191
|
+
// A lead listens while holding no work of their own: a monitoring task is the only task.
|
|
1192
|
+
if (Array.isArray(snapshot.tasks) && snapshot.tasks.length > 1) {
|
|
1193
|
+
snapshot.tasks.forEach((task, index) => {
|
|
1194
|
+
if (isPlainObject(task) && task.monitoring !== undefined) {
|
|
1195
|
+
into.add("snapshot.monitoring.alone", `${path}.tasks[${index}].monitoring`, "a lead listens only while holding no task of their own");
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1164
1199
|
// A break in effect begins when the work ends, so it holds no task. A snapshot reporting both
|
|
1165
|
-
// describes a state the agent cannot be in, whichever half is stale.
|
|
1200
|
+
// describes a state the agent cannot be in, whichever half is stale. The one exception is a
|
|
1201
|
+
// lead listening to a call during a break that is work of another sort -- coaching,
|
|
1202
|
+
// administrative, training -- and on no other.
|
|
1166
1203
|
if (isPlainObject(snapshot.break) && snapshot.break.approval === "in-effect"
|
|
1167
1204
|
&& Array.isArray(snapshot.tasks) && snapshot.tasks.length > 0) {
|
|
1168
|
-
|
|
1205
|
+
const listening = snapshot.tasks.every((task) => isPlainObject(task) && task.monitoring !== undefined);
|
|
1206
|
+
if (!listening) {
|
|
1207
|
+
into.add("break.in-effect.tasks", `${path}.tasks`, "a break in effect holds no task: it begins when the work ends, and until then the state is starting-after-task");
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
const state = snapshot.break;
|
|
1211
|
+
const reasons = Array.isArray(state.reasons) ? state.reasons : [];
|
|
1212
|
+
const active = reasons.find((reason) => isPlainObject(reason) && reason.id === state.activeReasonId);
|
|
1213
|
+
const kind = isPlainObject(active) ? active.kind : undefined;
|
|
1214
|
+
into.require(typeof kind === "string" && MONITORING_BREAK_KINDS.includes(kind), "snapshot.monitoring.break", `${path}.tasks`, `a lead listens during a ${MONITORING_BREAK_KINDS.join(", ")} break and no other; the active reason is ${kind === undefined ? "unstated" : String(kind)}`);
|
|
1215
|
+
}
|
|
1169
1216
|
}
|
|
1170
1217
|
// Presence is the permission, and it cuts both ways: data a provider never declared a
|
|
1171
1218
|
// capability for is data Omni would show against a control the agent does not have.
|
|
@@ -1622,6 +1669,7 @@ const RESULT_STATUSES = {
|
|
|
1622
1669
|
endBreak: { success: "ended", failure: "failed" },
|
|
1623
1670
|
executeTeamBreak: { success: "applied", failure: "failed" },
|
|
1624
1671
|
executeTeamLeadAssist: { success: "applied", failure: "failed" },
|
|
1672
|
+
executeTeamMonitor: { success: "applied", failure: "failed" },
|
|
1625
1673
|
openMedia: { success: "opened", failure: "unavailable" },
|
|
1626
1674
|
setPreference: { success: "applied", failure: "failed" },
|
|
1627
1675
|
recordStep: { success: "recorded", failure: "failed" },
|
|
@@ -1708,6 +1756,10 @@ function validateUserCapabilitiesInto(value, path, into, levels) {
|
|
|
1708
1756
|
continue;
|
|
1709
1757
|
if (!into.require(TEAM_CAPABILITIES.includes(control), "authentication.capability.team.unknown", `${path}.team.${control}`, `unsupported team capability: ${control}`))
|
|
1710
1758
|
continue;
|
|
1759
|
+
if (control === "monitorControl") {
|
|
1760
|
+
validateMonitorControl(on, `${path}.team.monitorControl`, into);
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1711
1763
|
into.require(on === true, "authentication.capability.value", `${path}.team.${control}`, `${control} is declared by presence: send true or omit it`);
|
|
1712
1764
|
}
|
|
1713
1765
|
continue;
|
|
@@ -1715,6 +1767,22 @@ function validateUserCapabilitiesInto(value, path, into, levels) {
|
|
|
1715
1767
|
into.require(declared === true, "authentication.capability.value", `${path}.${name}`, `${name} is declared by presence: send true or omit it`);
|
|
1716
1768
|
}
|
|
1717
1769
|
}
|
|
1770
|
+
/**
|
|
1771
|
+
* The modes a lead may listen in, as a list: whisper and barge begin from a monitor, so a list
|
|
1772
|
+
* without `monitor` names modes the lead could never reach.
|
|
1773
|
+
*/
|
|
1774
|
+
function validateMonitorControl(value, path, into) {
|
|
1775
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1776
|
+
into.add("authentication.capability.team.monitorControl.shape", path, "monitorControl lists the modes this lead may listen in: a non-empty array, or omitted for a lead who may not");
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
value.forEach((mode, index) => {
|
|
1780
|
+
if (into.oneOf(mode, MONITOR_MODES, "authentication.capability.team.monitorControl.mode", `${path}[${index}]`) && value.indexOf(mode) !== index) {
|
|
1781
|
+
into.add("authentication.capability.team.monitorControl.unique", `${path}[${index}]`, `duplicate monitor mode: ${String(mode)}`);
|
|
1782
|
+
}
|
|
1783
|
+
});
|
|
1784
|
+
into.require(value.includes("monitor"), "authentication.capability.team.monitorControl.monitor", path, "whisper and barge begin from a monitor: a lead who may listen in any mode may monitor");
|
|
1785
|
+
}
|
|
1718
1786
|
export function validateAuthenticationState(state, path = "authentication", context = {}) {
|
|
1719
1787
|
const into = new Collector();
|
|
1720
1788
|
if (!isPlainObject(state)) {
|
package/guide.md
CHANGED
|
@@ -36,6 +36,7 @@ are used precisely throughout and mean nothing looser here.
|
|
|
36
36
|
| **Break** | A reported, supervised state in which the agent is not working — one with a reason, a decision behind it and a return. It covers what a platform may call *not-ready*, including equipment trouble. An agent who is merely at capacity is not on a break. |
|
|
37
37
|
| **Workspace** | What Omni shows the agent. The **task workspace** holds the selected task, its controls and its browsers; the **idle workspace** holds what a provider contributes when no task is selected — dialpad, contacts, calendar, roster. |
|
|
38
38
|
| **Dial** | One outbound call the host asks a provider to place — from the idle dialpad, a transfer, a conference add, or a connect-back. Identified by the host's `dialId`, accepted as `dialling`, and ended by exactly one `dial-outcome`. See **Every dial has an outcome**. |
|
|
39
|
+
| **Monitor** | A lead listening to a member's call unasked, from the roster: `monitor` in silence, `whisper` heard by the agent alone, `barge` heard by everyone. Nothing of it reaches the member's task, and there is no take-over in it. See **Monitoring a call**. |
|
|
39
40
|
| **On the call** | Who a voice task's audio joins, or is bringing in, as the provider states it on `Task.onCall`: the party, the agents, and anyone consulted or conferenced in from the moment their dial is placed. |
|
|
40
41
|
|
|
41
42
|
Six words describe *what state a thing is in*, and they are not interchangeable: each belongs to
|
|
@@ -229,9 +230,12 @@ type AuthenticationContext = {
|
|
|
229
230
|
log?: (entry: unknown) => void;
|
|
230
231
|
};
|
|
231
232
|
|
|
233
|
+
type MonitorMode = "monitor" | "whisper" | "barge";
|
|
234
|
+
|
|
232
235
|
type TeamCapabilities = {
|
|
233
236
|
breakControl?: true;
|
|
234
237
|
leadAssistControl?: true;
|
|
238
|
+
monitorControl?: MonitorMode[];
|
|
235
239
|
policyControl?: true;
|
|
236
240
|
};
|
|
237
241
|
|
|
@@ -610,6 +614,13 @@ type TaskAssisting = {
|
|
|
610
614
|
since: IsoTimestamp;
|
|
611
615
|
};
|
|
612
616
|
|
|
617
|
+
type TaskMonitoring = {
|
|
618
|
+
memberId: UserId;
|
|
619
|
+
taskId: TaskId;
|
|
620
|
+
mode: MonitorMode;
|
|
621
|
+
since: IsoTimestamp;
|
|
622
|
+
};
|
|
623
|
+
|
|
613
624
|
type Level = string;
|
|
614
625
|
|
|
615
626
|
type LevelDeclaration = {
|
|
@@ -641,8 +652,8 @@ type Task<C extends Channel = Channel> = {
|
|
|
641
652
|
handlingHistory?: TaskHandlingHistory;
|
|
642
653
|
} & TaskCompletion & (
|
|
643
654
|
C extends "voice"
|
|
644
|
-
? { onCall?: OnCall[]; leadAssist?: TaskLeadAssist; assisting?: TaskAssisting; media?: TaskMediaState }
|
|
645
|
-
: { onCall?: never; leadAssist?: never; assisting?: never; media?: never }
|
|
655
|
+
? { onCall?: OnCall[]; leadAssist?: TaskLeadAssist; assisting?: TaskAssisting; monitoring?: TaskMonitoring; media?: TaskMediaState }
|
|
656
|
+
: { onCall?: never; leadAssist?: never; assisting?: never; monitoring?: never; media?: never }
|
|
646
657
|
);
|
|
647
658
|
|
|
648
659
|
type AcceptanceMode =
|
|
@@ -928,6 +939,16 @@ type TeamLeadAssistCommandRequest = {
|
|
|
928
939
|
command: TeamLeadAssistCommand;
|
|
929
940
|
};
|
|
930
941
|
|
|
942
|
+
type TeamMonitorCommand =
|
|
943
|
+
| { type: "monitor"; memberId: UserId }
|
|
944
|
+
| { type: "whisper" }
|
|
945
|
+
| { type: "barge" }
|
|
946
|
+
| { type: "leave" };
|
|
947
|
+
|
|
948
|
+
type TeamMonitorCommandRequest = {
|
|
949
|
+
command: TeamMonitorCommand;
|
|
950
|
+
};
|
|
951
|
+
|
|
931
952
|
type TeamCommandResult =
|
|
932
953
|
| { status: "applied" }
|
|
933
954
|
| { status: "failed"; failure: ProtocolFailure };
|
|
@@ -1022,6 +1043,7 @@ type Connection<C extends Channel = Channel> = {
|
|
|
1022
1043
|
|
|
1023
1044
|
executeTeamBreak?(request: TeamBreakCommandRequest): Promise<TeamCommandResult>;
|
|
1024
1045
|
executeTeamLeadAssist?(request: TeamLeadAssistCommandRequest): Promise<TeamCommandResult>;
|
|
1046
|
+
executeTeamMonitor?(request: TeamMonitorCommandRequest): Promise<TeamCommandResult>;
|
|
1025
1047
|
openMedia?(request: OpenMediaRequest): Promise<OpenMediaResult>;
|
|
1026
1048
|
setPreference?(request: SetPreferenceRequest): Promise<PreferenceResult>;
|
|
1027
1049
|
recordStep?(report: HandlingReport): Promise<HandlingReportResult>;
|
|
@@ -1732,6 +1754,7 @@ them from what arrives later.
|
|
|
1732
1754
|
| `team` | This login leads a team. The provider publishes a `TeamRoster` to it on every snapshot — `[]` when nobody is in it — and to nobody else. |
|
|
1733
1755
|
| `team.breakControl` | This lead may act on their team's breaks through `executeTeamBreak` — place, release, decide, set policy — as far as the provider supports; a command it lacks answers `omni.capability-not-enabled`. Omni asks for a decision only against a member whose `break` is `awaiting-decision`, so a provider that grants on request is never asked to decide. Requires `executeTeamBreak`. |
|
|
1734
1756
|
| `team.leadAssistControl` | This lead may join a member's call on request. Requires `executeTeamLeadAssist`. |
|
|
1757
|
+
| `team.monitorControl` | This lead may listen to a member's call unasked, in the listed modes and no others: `monitor`, `whisper`, `barge`. The list always includes `monitor`, since the other two begin from one. Requires `executeTeamMonitor`. See **Monitoring a call**. |
|
|
1735
1758
|
| `team.policyControl` | This lead sets the team's policy per capability — on, off, or the person's — within what the queue allows. Requires `executeTeamPolicy`; the roster carries `policies`. |
|
|
1736
1759
|
| `preferences` | What the team left to this person, with where each stands and who set it. Omitted when nothing was. Requires `setPreference`. See **Who decides what an agent may do**. |
|
|
1737
1760
|
|
|
@@ -1928,6 +1951,7 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
1928
1951
|
| `endBreak()` | The login declares `capabilities.breaks`. |
|
|
1929
1952
|
| `executeTeamBreak(command)` | The login declares `capabilities.team.breakControl`. |
|
|
1930
1953
|
| `executeTeamLeadAssist(command)` | The login declares `capabilities.team.leadAssistControl`. |
|
|
1954
|
+
| `executeTeamMonitor(command)` | The login declares `capabilities.team.monitorControl`. |
|
|
1931
1955
|
| `setPreference(request)` | The login declares `capabilities.preferences`: the person's choice has to have somewhere to go. |
|
|
1932
1956
|
| `recordStep(report)` | A task declares `mute`: the host performs that leg, and the provider's record has to have somewhere to take it. See **The host records what it performs**. |
|
|
1933
1957
|
| `executeTeamPolicy(command)` | The login declares `capabilities.team.policyControl`. |
|
|
@@ -2138,6 +2162,7 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2138
2162
|
| `onCall` | Voice only. Who is on the call, or being brought onto it, as the provider states it, replaced whole with the task: `party` is the customer, `agent` a person by user id, `consulted` and `conferenced` somebody a dial is bringing in, listed from the moment the dial is placed -- with the `destination` dialled, the `dialId` where a host placed it, the `stage` reached (`ringing` until answered, `joined` after), and `held: true` on anyone joined and parked. A `consulted` entry is what makes `transfer` `complete` and `cancel` issuable. `label` names a destination -- a person, a queue -- not a phrase; the host supplies the verb. Present when the provider knows the room, absent when it does not. See **Every dial has an outcome**. |
|
|
2139
2163
|
| `leadAssist` | Voice only. Present from the agent's request for a lead until the lead leaves or the request ends: `requested` while nobody has joined, `joined` with the lead's `leadId` once somebody has. See **Lead assist**. |
|
|
2140
2164
|
| `assisting` | Voice only, on the lead's own task for a call they joined: which member asked, with their note. Its presence is what makes `lead-assist` `take-over` and `leave` issuable. See **Lead assist**. |
|
|
2165
|
+
| `monitoring` | Voice only, on the lead's own task while they listen to a member's call: whose call, which call, and the `mode` they are heard in, restated on every change. Never on the member's task, and never together with `assisting`. See **Monitoring a call**. |
|
|
2141
2166
|
|
|
2142
2167
|
`TaskAttribute` entries carry typed detail alongside the task:
|
|
2143
2168
|
|
|
@@ -2908,7 +2933,7 @@ rendering one as the other tells an agent to wait for somebody who is never comi
|
|
|
2908
2933
|
| `awaiting-decision` | A person has to decide. The agent is waiting on somebody. |
|
|
2909
2934
|
| `granted` | A person decided yes. Omni may now tell this provider to stop the agent; until it does, work continues normally, and this says nothing about why Omni has not. |
|
|
2910
2935
|
| `starting-after-task` | Omni has told the provider to stop; the break begins when the current task ends. No new work arrives meanwhile, and nobody needs to act. |
|
|
2911
|
-
| `in-effect` | The agent is on the break now. It holds no task: a break begins when the work ends, so a snapshot reporting `in-effect` beside a task is refused as `break.in-effect.tasks`. |
|
|
2936
|
+
| `in-effect` | The agent is on the break now. It holds no task: a break begins when the work ends, so a snapshot reporting `in-effect` beside a task is refused as `break.in-effect.tasks`. The one exception is a lead's monitoring task during a `coaching`, `administrative` or `training` break -- see **Monitoring a call**. |
|
|
2912
2937
|
|
|
2913
2938
|
A denial is a decision, not a standing approval state. The provider transitions the request directly
|
|
2914
2939
|
to `not-requested`; Omni returns the agent to idle and never asks again on their behalf. They saw the
|
|
@@ -3400,6 +3425,73 @@ const leadAssistCapable = {
|
|
|
3400
3425
|
Lead and member alike are `UserId`s of this provider, so an adapter publishing them implements
|
|
3401
3426
|
`describeUsers()`; names never travel on a task or a roster.
|
|
3402
3427
|
|
|
3428
|
+
### Monitoring a call
|
|
3429
|
+
|
|
3430
|
+
A lead may listen to a member's call without being asked -- to coach, to check quality, to step in
|
|
3431
|
+
when it goes wrong. This is not lead assist: nobody requested it, the member's task says nothing
|
|
3432
|
+
about it, and there is no take-over in it. A lead who wants the call itself uses lead assist. The
|
|
3433
|
+
three modes are the call centre's own words:
|
|
3434
|
+
|
|
3435
|
+
| Mode | Who hears the lead |
|
|
3436
|
+
| --- | --- |
|
|
3437
|
+
| `monitor` | Nobody. The lead hears both sides in silence. |
|
|
3438
|
+
| `whisper` | The agent alone. The customer hears nothing. |
|
|
3439
|
+
| `barge` | Everyone on the call. |
|
|
3440
|
+
|
|
3441
|
+
The login says which modes this lead has, as a list on `team.monitorControl`, and it always
|
|
3442
|
+
includes `monitor`, because whisper and barge begin from one
|
|
3443
|
+
(`authentication.capability.team.monitorControl.monitor`). A centre that lets every lead listen but
|
|
3444
|
+
reserves barging declares `["monitor", "whisper"]`, and Omni offers no Barge to that lead. The
|
|
3445
|
+
declaration requires `executeTeamMonitor`, gated exactly as `executeTeamLeadAssist` is by
|
|
3446
|
+
`team.leadAssistControl`.
|
|
3447
|
+
|
|
3448
|
+
```ts
|
|
3449
|
+
// 1. The lead picks a member from the roster and starts silent.
|
|
3450
|
+
executeTeamMonitor({ command: { type: "monitor", memberId: "A-1" } })
|
|
3451
|
+
|
|
3452
|
+
// 2. The lead's own task arrives -- task-offered with `automatic`, as a joined call does -- and
|
|
3453
|
+
// carries `monitoring`; the task id is the lead's, the member's call is named inside it.
|
|
3454
|
+
// monitoring: { memberId: "A-1", taskId: "call-42", mode: "monitor", since }
|
|
3455
|
+
|
|
3456
|
+
// 3. The lead changes how they are heard; the provider restates the task with the new mode.
|
|
3457
|
+
executeTeamMonitor({ command: { type: "whisper" } })
|
|
3458
|
+
executeTeamMonitor({ command: { type: "barge" } })
|
|
3459
|
+
|
|
3460
|
+
// 4. The lead leaves; their task ends with `{ type: "left" }`. The member's call goes on.
|
|
3461
|
+
executeTeamMonitor({ command: { type: "leave" } })
|
|
3462
|
+
```
|
|
3463
|
+
|
|
3464
|
+
**The lead's task is what gives them audio**: Omni opens media on it as on any voice task, and the
|
|
3465
|
+
provider bridges the lead's leg into the member's call in the mode stated. `monitoring.mode` is
|
|
3466
|
+
the state, restated on every change, and a `whisper` or `barge` the login's list does not include
|
|
3467
|
+
is answered `failed`. **A lead listens to one call at a time** (`snapshot.monitoring.single`), and
|
|
3468
|
+
a task is a joined call or a monitored one, never both (`task.monitoring.assisting`). The member's
|
|
3469
|
+
call ending ends the lead's task as it ends the member's, with `left`, since the lead was never
|
|
3470
|
+
handling it.
|
|
3471
|
+
|
|
3472
|
+
**A lead listens only while holding no work of their own.** A monitoring task is the lead's only
|
|
3473
|
+
task, and a snapshot carrying one beside any other is refused (`snapshot.monitoring.alone`); Omni
|
|
3474
|
+
offers Monitor to a lead with no task. That includes a lead on a break -- unlike joining, which a
|
|
3475
|
+
break forbids -- but only a break that is work of another sort: `coaching`, `administrative` or
|
|
3476
|
+
`training`, the kinds in `MONITORING_BREAK_KINDS`, and never a meal or a rest. A break in effect
|
|
3477
|
+
otherwise holds no task; the monitoring task is its one exception, and a snapshot carrying one
|
|
3478
|
+
during a break whose active reason is of any other kind, or of no stated kind, is refused
|
|
3479
|
+
(`snapshot.monitoring.break`). The kind decides, not the reason's name: a centre that wants leads
|
|
3480
|
+
listening during a break declares that break `coaching`, `administrative` or `training`.
|
|
3481
|
+
|
|
3482
|
+
**Nothing reaches the member.** The member's task carries no trace of a monitoring lead in any
|
|
3483
|
+
mode: not on `onCall`, not in the record. Whether a whisper is announced to the agent is the
|
|
3484
|
+
platform's business and travels on the audio, not on this wire.
|
|
3485
|
+
|
|
3486
|
+
```ts
|
|
3487
|
+
const monitoringLead = {
|
|
3488
|
+
channel: "voice",
|
|
3489
|
+
capabilities: {},
|
|
3490
|
+
phase: "in-progress",
|
|
3491
|
+
monitoring: { memberId: "A-1", taskId: "call-42", mode: "whisper", since: "2026-08-21T09:04:00Z" },
|
|
3492
|
+
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "monitoring">;
|
|
3493
|
+
```
|
|
3494
|
+
|
|
3403
3495
|
### A member waiting for a break
|
|
3404
3496
|
|
|
3405
3497
|
A member who has asked for a break **keeps working** until Omni commits it, so asking is not an
|