@xema/omni-protocol 0.1.88 → 0.1.90
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 +26 -26
- package/dist/index.js +7 -7
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +10 -10
- package/dist/validation.d.ts +1 -1
- package/dist/validation.js +42 -41
- package/guide.md +86 -59
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -180,7 +180,7 @@ export interface Manifest<C extends Channel = Channel> {
|
|
|
180
180
|
*/
|
|
181
181
|
runningStepReports?: true;
|
|
182
182
|
/**
|
|
183
|
-
* How long after an applied completion -- `complete`, or a lead's `take-over` -- the provider's
|
|
183
|
+
* How long after an applied completion -- `complete`, or a lead's `take-over-call` -- the provider's
|
|
184
184
|
* `task-ended` is owed, in milliseconds. A warm transfer's `complete` is not one: the agent's
|
|
185
185
|
* wrap runs after it as after any call. `applied` says the provider
|
|
186
186
|
* has completed the task; the ending follows within this, or the host resyncs and shows the
|
|
@@ -245,19 +245,19 @@ export interface TeamCapabilities {
|
|
|
245
245
|
leadAssistControl?: true;
|
|
246
246
|
/**
|
|
247
247
|
* This lead may listen to a member's call unasked, in these modes and no others. Coach and
|
|
248
|
-
* join-call begin
|
|
249
|
-
* member's task. Requires `
|
|
248
|
+
* join-call begin with silent listening, so the list always includes `listen`. Nothing of it reaches the
|
|
249
|
+
* member's task. Requires `executeTeamListen`.
|
|
250
250
|
*/
|
|
251
|
-
|
|
251
|
+
listeningControl?: ListeningMode[];
|
|
252
252
|
/** This lead sets the team's policy per capability -- on, off, or the agent's -- within what the queue allows. Requires `executeTeamPolicy`. */
|
|
253
253
|
policyControl?: true;
|
|
254
254
|
}
|
|
255
255
|
/**
|
|
256
|
-
* How a lead listening to a member's call is heard: `
|
|
256
|
+
* How a lead listening to a member's call is heard: `listen` hears both sides in silence,
|
|
257
257
|
* `coach` is heard by the agent alone, `join-call` by everyone on the call.
|
|
258
258
|
*/
|
|
259
|
-
export type
|
|
260
|
-
export declare const
|
|
259
|
+
export type ListeningMode = "listen" | "coach" | "join-call";
|
|
260
|
+
export declare const LISTENING_MODES: readonly ["listen", "coach", "join-call"];
|
|
261
261
|
/**
|
|
262
262
|
* What this login may do, beyond any one task. It travels with the identity because it is part
|
|
263
263
|
* of who the agent is on this provider: the provider knows the roles and says so at sign-in.
|
|
@@ -879,7 +879,7 @@ export type TaskLeadAssist = {
|
|
|
879
879
|
});
|
|
880
880
|
/**
|
|
881
881
|
* On the lead's own task for a call they joined: which member asked, with their note. Its
|
|
882
|
-
* presence is what makes `lead-assist` `take-over` and `leave` issuable.
|
|
882
|
+
* presence is what makes `lead-assist` `take-over-call` and `leave` issuable.
|
|
883
883
|
*/
|
|
884
884
|
export interface TaskAssisting {
|
|
885
885
|
memberId: UserId;
|
|
@@ -891,13 +891,13 @@ export interface TaskAssisting {
|
|
|
891
891
|
* lead is heard, restated on every change of mode. Nothing of it reaches the member's task. There
|
|
892
892
|
* is no take-over here; a lead who wants the call uses lead assist. It is the lead's only task:
|
|
893
893
|
* a lead listens while holding no work of their own, idle or on a break of a kind in
|
|
894
|
-
* `
|
|
894
|
+
* `LISTENING_BREAK_KINDS`.
|
|
895
895
|
*/
|
|
896
|
-
export interface
|
|
896
|
+
export interface TaskListening {
|
|
897
897
|
memberId: UserId;
|
|
898
898
|
taskId: TaskId;
|
|
899
899
|
allocationId: AllocationId;
|
|
900
|
-
mode:
|
|
900
|
+
mode: ListeningMode;
|
|
901
901
|
since: IsoTimestamp;
|
|
902
902
|
}
|
|
903
903
|
/**
|
|
@@ -976,7 +976,7 @@ export type Task<C extends Channel = Channel> = {
|
|
|
976
976
|
* snapshot says it too: an offer the host never received is not accepted on the person's behalf
|
|
977
977
|
* for want of a word. Required while `pending` when Omni said it may auto-accept
|
|
978
978
|
* (`autoAcceptTasks: true`) or the work is the agent's own -- a dial, a connect-back, a join, a
|
|
979
|
-
*
|
|
979
|
+
* listen, which then say `automatic` under either provisioning -- forbidden on routed work when
|
|
980
980
|
* Omni said not, and absent past `pending`.
|
|
981
981
|
*/
|
|
982
982
|
acceptance?: AcceptanceMode;
|
|
@@ -999,14 +999,14 @@ export type Task<C extends Channel = Channel> = {
|
|
|
999
999
|
onCall?: OnCall[];
|
|
1000
1000
|
leadAssist?: TaskLeadAssist;
|
|
1001
1001
|
assisting?: TaskAssisting;
|
|
1002
|
-
|
|
1002
|
+
listening?: TaskListening;
|
|
1003
1003
|
media?: TaskMediaState;
|
|
1004
1004
|
} : {
|
|
1005
1005
|
recording?: never;
|
|
1006
1006
|
onCall?: never;
|
|
1007
1007
|
leadAssist?: never;
|
|
1008
1008
|
assisting?: never;
|
|
1009
|
-
|
|
1009
|
+
listening?: never;
|
|
1010
1010
|
media?: never;
|
|
1011
1011
|
});
|
|
1012
1012
|
/**
|
|
@@ -1119,7 +1119,7 @@ export type VoiceTaskCommand = {
|
|
|
1119
1119
|
/** The lead keeps the customer; the agent's task ends `transferred`. Needs `Task.assisting`. */
|
|
1120
1120
|
| {
|
|
1121
1121
|
type: "lead-assist";
|
|
1122
|
-
action: "take-over";
|
|
1122
|
+
action: "take-over-call";
|
|
1123
1123
|
}
|
|
1124
1124
|
/** The lead drops; the agent continues. The lead's task ends `left`. Needs `Task.assisting`. */
|
|
1125
1125
|
| {
|
|
@@ -1236,10 +1236,10 @@ export declare const BREAK_KINDS: readonly ["short-break", "meal", "rest", "trai
|
|
|
1236
1236
|
export type BreakKind = (typeof BREAK_KINDS)[number];
|
|
1237
1237
|
/**
|
|
1238
1238
|
* The breaks during which a lead may listen to a member's call: work of another sort, not rest.
|
|
1239
|
-
* A
|
|
1239
|
+
* A listening task is the one task a break in effect may hold, and only on one of these.
|
|
1240
1240
|
*/
|
|
1241
|
-
export declare const
|
|
1242
|
-
export declare const
|
|
1241
|
+
export declare const LISTENING_BREAK_KINDS: readonly ["coaching", "administrative", "training"];
|
|
1242
|
+
export declare const breakKindAllowsListening: (kind: BreakKind | undefined) => boolean;
|
|
1243
1243
|
export interface BreakReason {
|
|
1244
1244
|
id: string;
|
|
1245
1245
|
label: string;
|
|
@@ -1372,12 +1372,12 @@ export interface TeamLeadAssistCommandRequest {
|
|
|
1372
1372
|
command: TeamLeadAssistCommand;
|
|
1373
1373
|
}
|
|
1374
1374
|
/**
|
|
1375
|
-
* A lead listening to a member's call. `
|
|
1375
|
+
* A lead listening to a member's call. `listen` starts one, silent, on a member from the team member list;
|
|
1376
1376
|
* `coach` and `join-call` change how the standing one is heard; `leave` ends it. One member at a
|
|
1377
|
-
* time, and only the modes the login's `
|
|
1377
|
+
* time, and only the modes the login's `listeningControl` lists.
|
|
1378
1378
|
*/
|
|
1379
|
-
export type
|
|
1380
|
-
type: "
|
|
1379
|
+
export type TeamListenCommand = {
|
|
1380
|
+
type: "listen";
|
|
1381
1381
|
memberId: UserId;
|
|
1382
1382
|
} | {
|
|
1383
1383
|
type: "coach";
|
|
@@ -1386,8 +1386,8 @@ export type TeamMonitorCommand = {
|
|
|
1386
1386
|
} | {
|
|
1387
1387
|
type: "leave";
|
|
1388
1388
|
};
|
|
1389
|
-
export interface
|
|
1390
|
-
command:
|
|
1389
|
+
export interface TeamListenCommandRequest {
|
|
1390
|
+
command: TeamListenCommand;
|
|
1391
1391
|
}
|
|
1392
1392
|
export type TeamBreakCommand = {
|
|
1393
1393
|
type: "decide";
|
|
@@ -1709,8 +1709,8 @@ export interface Connection<C extends Channel = Channel> {
|
|
|
1709
1709
|
executeTeamBreak?(request: TeamBreakCommandRequest): Promise<TeamCommandResult>;
|
|
1710
1710
|
/** Required when the login declares `capabilities.team.leadAssistControl`. */
|
|
1711
1711
|
executeTeamLeadAssist?(request: TeamLeadAssistCommandRequest): Promise<TeamCommandResult>;
|
|
1712
|
-
/** Required when the login declares `capabilities.team.
|
|
1713
|
-
|
|
1712
|
+
/** Required when the login declares `capabilities.team.listeningControl`. */
|
|
1713
|
+
executeTeamListen?(request: TeamListenCommandRequest): Promise<TeamCommandResult>;
|
|
1714
1714
|
/** Required when the login declares `capabilities.team.policyControl`. */
|
|
1715
1715
|
executeTeamPolicy?(request: TeamPolicyCommandRequest): Promise<TeamCommandResult>;
|
|
1716
1716
|
/** Required of a voice adapter whose manifest lists `softphone`: on one, the call's audio lands in Omni. */
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ export const IDLE_CAPABILITY_UI = {
|
|
|
26
26
|
calendar: "Calendar",
|
|
27
27
|
contacts: "Contacts",
|
|
28
28
|
};
|
|
29
|
-
export const
|
|
29
|
+
export const LISTENING_MODES = ["listen", "coach", "join-call"];
|
|
30
30
|
export const MUTED_BY = ["host", "station"];
|
|
31
31
|
export const HOST_MUTES = ["stream", "station"];
|
|
32
32
|
export const RECORDING_ACTIONS = ["start", "pause", "resume", "stop", "cancel"];
|
|
@@ -97,10 +97,10 @@ export const BREAK_KINDS = [
|
|
|
97
97
|
];
|
|
98
98
|
/**
|
|
99
99
|
* The breaks during which a lead may listen to a member's call: work of another sort, not rest.
|
|
100
|
-
* A
|
|
100
|
+
* A listening task is the one task a break in effect may hold, and only on one of these.
|
|
101
101
|
*/
|
|
102
|
-
export const
|
|
103
|
-
export const
|
|
102
|
+
export const LISTENING_BREAK_KINDS = ["coaching", "administrative", "training"];
|
|
103
|
+
export const breakKindAllowsListening = (kind) => kind !== undefined && LISTENING_BREAK_KINDS.includes(kind);
|
|
104
104
|
export const OMNI_FAILURE_CODES = [
|
|
105
105
|
"omni.not-authenticated",
|
|
106
106
|
"omni.capability-not-enabled",
|
|
@@ -180,15 +180,15 @@ export function interactionStepExpectsAPerson(step) {
|
|
|
180
180
|
* not the thing being compared. Key order does not matter, and `team: {}` is not `team` absent.
|
|
181
181
|
*/
|
|
182
182
|
export function sameCapabilities(a, b) {
|
|
183
|
-
const modes = (team) => team?.
|
|
183
|
+
const modes = (team) => team?.listeningControl === undefined ? undefined : [...team.listeningControl].sort().join(",");
|
|
184
184
|
// Every field, so a capability added later cannot be missed here: `satisfies` pins the field
|
|
185
185
|
// lists to the types, and a new key is a compile error until it is compared.
|
|
186
|
-
const teamKeys = { breakControl: true, leadAssistControl: true,
|
|
186
|
+
const teamKeys = { breakControl: true, leadAssistControl: true, listeningControl: true, policyControl: true };
|
|
187
187
|
void { breaks: true, preferences: true, team: true };
|
|
188
188
|
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
189
|
return a.breaks === b.breaks &&
|
|
190
190
|
(a.team === undefined) === (b.team === undefined) &&
|
|
191
|
-
Object.keys(teamKeys).every(key => key === "
|
|
191
|
+
Object.keys(teamKeys).every(key => key === "listeningControl" || a.team?.[key] === b.team?.[key]) &&
|
|
192
192
|
modes(a.team) === modes(b.team) &&
|
|
193
193
|
preferences(a.preferences) === preferences(b.preferences);
|
|
194
194
|
}
|
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.interactionHistory", "task.onCall", "task.leadAssist", "task.assisting", "task.
|
|
10
|
+
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.interactionHistory", "task.onCall", "task.leadAssist", "task.assisting", "task.listening", "task.media", "task.acceptance", "task.outcomes", "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,7 +15,7 @@ const STATE_SUBJECTS = [
|
|
|
15
15
|
"task.onCall",
|
|
16
16
|
"task.leadAssist",
|
|
17
17
|
"task.assisting",
|
|
18
|
-
"task.
|
|
18
|
+
"task.listening",
|
|
19
19
|
"task.media",
|
|
20
20
|
"task.acceptance",
|
|
21
21
|
"task.outcomes",
|
|
@@ -61,8 +61,8 @@ function observeTask(value, seen) {
|
|
|
61
61
|
seen.add("task.leadAssist");
|
|
62
62
|
if (value.assisting !== undefined)
|
|
63
63
|
seen.add("task.assisting");
|
|
64
|
-
if (value.
|
|
65
|
-
seen.add("task.
|
|
64
|
+
if (value.listening !== undefined)
|
|
65
|
+
seen.add("task.listening");
|
|
66
66
|
if (value.media !== undefined)
|
|
67
67
|
seen.add("task.media");
|
|
68
68
|
if (value.acceptance !== undefined)
|
|
@@ -233,8 +233,8 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
233
233
|
requireMethod(on, "executeTeamBreak", "the login declares capabilities.team.breakControl");
|
|
234
234
|
if (capabilities.team?.leadAssistControl === true)
|
|
235
235
|
requireMethod(on, "executeTeamLeadAssist", "the login declares capabilities.team.leadAssistControl");
|
|
236
|
-
if (capabilities.team?.
|
|
237
|
-
requireMethod(on, "
|
|
236
|
+
if (capabilities.team?.listeningControl !== undefined)
|
|
237
|
+
requireMethod(on, "executeTeamListen", "the login declares capabilities.team.listeningControl");
|
|
238
238
|
if (capabilities.team?.policyControl === true)
|
|
239
239
|
requireMethod(on, "executeTeamPolicy", "the login declares capabilities.team.policyControl");
|
|
240
240
|
if (some(capabilities.preferences))
|
|
@@ -673,9 +673,9 @@ function userIdsIn(snapshot) {
|
|
|
673
673
|
add(t.leadAssist.leadId);
|
|
674
674
|
if (isRecord(t.assisting))
|
|
675
675
|
add(t.assisting.memberId);
|
|
676
|
-
if (isRecord(t.
|
|
677
|
-
add(t.
|
|
678
|
-
add(t.
|
|
676
|
+
if (isRecord(t.listening)) {
|
|
677
|
+
add(t.listening.memberId);
|
|
678
|
+
add(t.listening.agentId);
|
|
679
679
|
}
|
|
680
680
|
}
|
|
681
681
|
return [...ids];
|
|
@@ -684,7 +684,7 @@ const teamNamesUsers = (team) => isRecord(team) && (some(team.members) || some(t
|
|
|
684
684
|
const taskNamesUsers = (task) => isRecord(task) && ((isRecord(task.interactionHistory) && Array.isArray(task.interactionHistory.steps) && task.interactionHistory.steps.some(step => isRecord(step) && step.by !== undefined)) ||
|
|
685
685
|
(isRecord(task.leadAssist) && task.leadAssist.leadId !== undefined) ||
|
|
686
686
|
isRecord(task.assisting) ||
|
|
687
|
-
isRecord(task.
|
|
687
|
+
isRecord(task.listening));
|
|
688
688
|
/** Whether an event publishes a `UserId`, on a team member list, a task, or the snapshot a reconnect carries. */
|
|
689
689
|
function eventNamesUsers(envelope) {
|
|
690
690
|
const event = isRecord(envelope) ? envelope.event : undefined;
|
|
@@ -818,7 +818,7 @@ export function assertCapabilityWithdrawal(states, snapshot, manifest) {
|
|
|
818
818
|
(before.team !== undefined && after.team === undefined) ||
|
|
819
819
|
(before.team?.breakControl === true && after.team?.breakControl !== true) ||
|
|
820
820
|
(before.team?.leadAssistControl === true && after.team?.leadAssistControl !== true) ||
|
|
821
|
-
(before.team?.
|
|
821
|
+
(before.team?.listeningControl !== undefined && after.team?.listeningControl === undefined) ||
|
|
822
822
|
(before.team?.policyControl === true && after.team?.policyControl !== true) ||
|
|
823
823
|
(before.preferences ?? []).some(was => !(after.preferences ?? []).some(now => now.id === was.id));
|
|
824
824
|
if (!withdrawn) {
|
package/dist/validation.d.ts
CHANGED
|
@@ -135,7 +135,7 @@ export declare function validateTaskCommandRequest(request: unknown, task: unkno
|
|
|
135
135
|
*/
|
|
136
136
|
export declare function validateTaskCommand(command: unknown, task?: unknown, path?: string, taskContext?: Omit<TaskValidationContext, "channel">): ProtocolViolation[];
|
|
137
137
|
/** The connection methods whose results `validateResult` knows. */
|
|
138
|
-
export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeamBreak" | "executeTeamLeadAssist" | "
|
|
138
|
+
export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeamBreak" | "executeTeamLeadAssist" | "executeTeamListen" | "openMedia" | "setPreference" | "recordStep" | "executeTeamPolicy";
|
|
139
139
|
/**
|
|
140
140
|
* Validates what a connection method answered. A result is untrusted for the same reason a
|
|
141
141
|
* 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 { RECORDING_ACTIONS, ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS,
|
|
12
|
+
import { RECORDING_ACTIONS, ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, LISTENING_BREAK_KINDS, BROWSER_ISOLATION_SCHEMES, INTERACTION_STEPS_THAT_DIAL, IDLE_CAPABILITIES, OMNI_FAILURE_CODES, OMNI_SUPPORTED_PROTOCOL_VERSIONS, TASK_COMMAND_NAMES, 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") {
|
|
@@ -87,8 +87,8 @@ const OFFERABLE_PHASES = membersOf({
|
|
|
87
87
|
});
|
|
88
88
|
const PREVIEW_DEADLINES = membersOf({ calls: true, "host-calls": true, waits: true });
|
|
89
89
|
const PHONES = membersOf({ softphone: true, deskPhone: true });
|
|
90
|
-
const TEAM_CAPABILITIES = membersOf({ breakControl: true, leadAssistControl: true, policyControl: true,
|
|
91
|
-
const
|
|
90
|
+
const TEAM_CAPABILITIES = membersOf({ breakControl: true, leadAssistControl: true, policyControl: true, listeningControl: true });
|
|
91
|
+
const LISTENING_MODES = membersOf({ listen: true, coach: true, "join-call": true });
|
|
92
92
|
const COMPLETED_BY = membersOf({ agent: true, provider: true });
|
|
93
93
|
const CANCELLED_BY = membersOf({ agent: true, provider: true, party: true });
|
|
94
94
|
const EXPIRABLE_PHASES = membersOf({
|
|
@@ -1090,26 +1090,26 @@ function validateAssisting(value, channel, path, into) {
|
|
|
1090
1090
|
into.timestamp(value.since, "task.assisting.since", `${path}.since`);
|
|
1091
1091
|
}
|
|
1092
1092
|
/** The lead's own task while they listen: whose call, which call, and how they are heard. Voice only. */
|
|
1093
|
-
function
|
|
1093
|
+
function validateListening(value, channel, path, into) {
|
|
1094
1094
|
if (value === undefined)
|
|
1095
1095
|
return;
|
|
1096
|
-
if (!into.require(channel === "voice", "task.
|
|
1096
|
+
if (!into.require(channel === "voice", "task.listening.channel", path, `a ${channel} task has no call to listen to`))
|
|
1097
1097
|
return;
|
|
1098
1098
|
if (!isPlainObject(value)) {
|
|
1099
|
-
into.add("task.
|
|
1099
|
+
into.add("task.listening.shape", path, "listening must be an object when present");
|
|
1100
1100
|
return;
|
|
1101
1101
|
}
|
|
1102
|
-
into.require(isUserId(value.memberId), "task.
|
|
1103
|
-
into.require(isTaskId(value.taskId), "task.
|
|
1104
|
-
into.require(isAllocationId(value.allocationId), "task.
|
|
1105
|
-
into.oneOf(value.mode,
|
|
1106
|
-
into.timestamp(value.since, "task.
|
|
1102
|
+
into.require(isUserId(value.memberId), "task.listening.memberId", `${path}.memberId`, "a call being listened to names the member on it");
|
|
1103
|
+
into.require(isTaskId(value.taskId), "task.listening.taskId", `${path}.taskId`, "a call being listened to names the member's task");
|
|
1104
|
+
into.require(isAllocationId(value.allocationId), "task.listening.allocationId", `${path}.allocationId`, "a call being listened to names the member's task by its allocation too");
|
|
1105
|
+
into.oneOf(value.mode, LISTENING_MODES, "task.listening.mode", `${path}.mode`);
|
|
1106
|
+
into.timestamp(value.since, "task.listening.since", `${path}.since`);
|
|
1107
1107
|
}
|
|
1108
|
-
/** Whether a task is the agent's own doing: a dial the host placed on its onCall, a lead's join, a lead's
|
|
1108
|
+
/** Whether a task is the agent's own doing: a dial the host placed on its onCall, a lead's join, a lead's listen. */
|
|
1109
1109
|
function originatedByTheAgent(task) {
|
|
1110
1110
|
if (Array.isArray(task.onCall) && task.onCall.some(entry => isPlainObject(entry) && typeof entry.dialId === "string"))
|
|
1111
1111
|
return true;
|
|
1112
|
-
return task.assisting !== undefined || task.
|
|
1112
|
+
return task.assisting !== undefined || task.listening !== undefined;
|
|
1113
1113
|
}
|
|
1114
1114
|
export function validateTask(task, context, path = "task") {
|
|
1115
1115
|
const into = new Collector();
|
|
@@ -1121,6 +1121,7 @@ function validateTaskInto(task, context, path, into) {
|
|
|
1121
1121
|
into.add("task.shape", path, "a task must be an object");
|
|
1122
1122
|
return;
|
|
1123
1123
|
}
|
|
1124
|
+
into.require(!Object.hasOwn(task, "monitoring"), "task.listening.renamed", `${path}.monitoring`, "use listening; the former field is not accepted");
|
|
1124
1125
|
into.require(!Object.hasOwn(task, "handlingHistory"), "task.interactionHistory.renamed", `${path}.handlingHistory`, "use interactionHistory; the former field is not accepted");
|
|
1125
1126
|
into.require(isTaskId(task.id), "task.id", `${path}.id`, "a task needs a non-empty id");
|
|
1126
1127
|
into.require(isAllocationId(task.allocationId), "task.allocationId", `${path}.allocationId`, "a task names this life of itself: an allocation id minted once per offer, never reused for the life of the login");
|
|
@@ -1151,7 +1152,7 @@ function validateTaskInto(task, context, path, into) {
|
|
|
1151
1152
|
// Acceptance is the offer's word, carried on the pending task so a snapshot can say it: it
|
|
1152
1153
|
// travels exactly when Omni said tasks may be auto-accepted, and only while the task is pending.
|
|
1153
1154
|
// Work the agent originated -- a dial or connect-back, carrying the host's dialId on onCall; a
|
|
1154
|
-
// lead's join, carrying assisting; a
|
|
1155
|
+
// lead's join, carrying assisting; a listen, carrying listening -- was accepted by the command
|
|
1155
1156
|
// that created it, and says so whatever the provisioning: the desk shows no Accept for a call
|
|
1156
1157
|
// the agent placed. The provisioning governs work the queue routes, and nothing else.
|
|
1157
1158
|
const originated = task.phase === "pending" && originatedByTheAgent(task);
|
|
@@ -1162,14 +1163,14 @@ function validateTaskInto(task, context, path, into) {
|
|
|
1162
1163
|
into.add("task.acceptance.unexpected", `${path}.acceptance`, "acceptance is an offer's word; a task past pending has been accepted");
|
|
1163
1164
|
}
|
|
1164
1165
|
else if (originated) {
|
|
1165
|
-
into.require(task.acceptance === "automatic", "task.acceptance.originated", `${path}.acceptance`, "work the agent originated -- a dial, a connect-back, a join, a
|
|
1166
|
+
into.require(task.acceptance === "automatic", "task.acceptance.originated", `${path}.acceptance`, "work the agent originated -- a dial, a connect-back, a join, a listen -- was accepted by the command that created it, and says automatic whatever the provisioning");
|
|
1166
1167
|
}
|
|
1167
1168
|
else if (context.autoAcceptTasks === false) {
|
|
1168
1169
|
into.add("task.acceptance.unexpected", `${path}.acceptance`, "autoAcceptTasks is off, so every task the queue routes requires agent acceptance and a pending task carries no acceptance");
|
|
1169
1170
|
}
|
|
1170
1171
|
}
|
|
1171
1172
|
else if (originated) {
|
|
1172
|
-
into.add("task.acceptance.originated", `${path}.acceptance`, "work the agent originated -- a dial, a connect-back, a join, a
|
|
1173
|
+
into.add("task.acceptance.originated", `${path}.acceptance`, "work the agent originated -- a dial, a connect-back, a join, a listen -- says acceptance: automatic whatever the provisioning: the command that created it accepted it");
|
|
1173
1174
|
}
|
|
1174
1175
|
else if (task.phase === "pending" && context.autoAcceptTasks === true) {
|
|
1175
1176
|
into.add("task.acceptance.required", `${path}.acceptance`, "autoAcceptTasks is on, so a pending task states how it is accepted");
|
|
@@ -1222,10 +1223,10 @@ function validateTaskInto(task, context, path, into) {
|
|
|
1222
1223
|
validateTaskMedia(task, task.media, context.channel, task.phase, `${path}.media`, into);
|
|
1223
1224
|
validateLeadAssist(task.leadAssist, context.channel, `${path}.leadAssist`, into);
|
|
1224
1225
|
validateAssisting(task.assisting, context.channel, `${path}.assisting`, into);
|
|
1225
|
-
|
|
1226
|
+
validateListening(task.listening, context.channel, `${path}.listening`, into);
|
|
1226
1227
|
// A lead's task is one thing: a call they joined on request, or a call they listen to unasked.
|
|
1227
|
-
if (task.assisting !== undefined && task.
|
|
1228
|
-
into.add("task.
|
|
1228
|
+
if (task.assisting !== undefined && task.listening !== undefined) {
|
|
1229
|
+
into.add("task.listening.assisting", `${path}.listening`, "a task is a joined call or a listened one, never both");
|
|
1229
1230
|
}
|
|
1230
1231
|
// Where the terms came from is stated with them: a host cannot tell "the platform permits
|
|
1231
1232
|
// nothing" from "the terms could not be read" from the set alone, and the provider knows which.
|
|
@@ -1334,7 +1335,7 @@ export function validateBreakStatus(state, tasks, path = "snapshot") {
|
|
|
1334
1335
|
}
|
|
1335
1336
|
if (isPlainObject(state) && state.approval === "in-effect"
|
|
1336
1337
|
&& Array.isArray(tasks) && tasks.length > 0) {
|
|
1337
|
-
const listening = tasks.every((task) => isPlainObject(task) && task.
|
|
1338
|
+
const listening = tasks.every((task) => isPlainObject(task) && task.listening !== undefined);
|
|
1338
1339
|
if (!listening) {
|
|
1339
1340
|
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");
|
|
1340
1341
|
}
|
|
@@ -1342,7 +1343,7 @@ export function validateBreakStatus(state, tasks, path = "snapshot") {
|
|
|
1342
1343
|
const reasons = Array.isArray(state.reasons) ? state.reasons : [];
|
|
1343
1344
|
const active = reasons.find((reason) => isPlainObject(reason) && reason.id === state.activeReasonId);
|
|
1344
1345
|
const kind = isPlainObject(active) ? active.kind : undefined;
|
|
1345
|
-
into.require(typeof kind === "string" &&
|
|
1346
|
+
into.require(typeof kind === "string" && LISTENING_BREAK_KINDS.includes(kind), "snapshot.listening.break", `${path}.tasks`, `a lead listens during a ${LISTENING_BREAK_KINDS.join(", ")} break and no other; the active reason is ${kind === undefined ? "unstated" : describeValue(kind)}`);
|
|
1346
1347
|
}
|
|
1347
1348
|
}
|
|
1348
1349
|
return into.violations;
|
|
@@ -1556,7 +1557,7 @@ function validateBreakState(value, path, into) {
|
|
|
1556
1557
|
}
|
|
1557
1558
|
// A break in effect, or about to be, on a provider that publishes reasons is on one of them: an
|
|
1558
1559
|
// imposed one included, since the lead's place named it. A break of no kind is a break whose
|
|
1559
|
-
// rules -- who may
|
|
1560
|
+
// rules -- who may listen through it, whether it counts -- nobody can apply.
|
|
1560
1561
|
ruleEvaluated("break.activeReasonId.required");
|
|
1561
1562
|
if (seen.size > 0 && (value.approval === "in-effect" || value.approval === "starting-after-task")) {
|
|
1562
1563
|
into.require(typeof value.activeReasonId === "string" && value.activeReasonId.length > 0, "break.activeReasonId.required", `${path}.activeReasonId`, `a break ${describeValue(value.approval)} on a provider that publishes reasons names the one it is on`);
|
|
@@ -1683,7 +1684,7 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot", context
|
|
|
1683
1684
|
else {
|
|
1684
1685
|
const seen = new Set();
|
|
1685
1686
|
let assisting;
|
|
1686
|
-
let
|
|
1687
|
+
let listening;
|
|
1687
1688
|
snapshot.tasks.forEach((task, index) => {
|
|
1688
1689
|
validateTaskInto(task, { channel, levels, autoAcceptTasks: context.autoAcceptTasks, dialOutcomesDeclared: manifestDials(manifest), locked: context.locked }, `${path}.tasks[${index}]`, into);
|
|
1689
1690
|
// A lead assists one call at a time, and listens to one at a time.
|
|
@@ -1692,10 +1693,10 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot", context
|
|
|
1692
1693
|
into.add("snapshot.assisting.single", `${path}.tasks[${index}].assisting`, "a lead assists one call at a time");
|
|
1693
1694
|
assisting = index;
|
|
1694
1695
|
}
|
|
1695
|
-
if (isPlainObject(task) && task.
|
|
1696
|
-
if (
|
|
1697
|
-
into.add("snapshot.
|
|
1698
|
-
|
|
1696
|
+
if (isPlainObject(task) && task.listening !== undefined) {
|
|
1697
|
+
if (listening !== undefined)
|
|
1698
|
+
into.add("snapshot.listening.single", `${path}.tasks[${index}].listening`, "a lead listens to one call at a time");
|
|
1699
|
+
listening = index;
|
|
1699
1700
|
}
|
|
1700
1701
|
if (isPlainObject(task) && isTaskId(task.id)) {
|
|
1701
1702
|
if (seen.has(task.id))
|
|
@@ -1704,11 +1705,11 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot", context
|
|
|
1704
1705
|
}
|
|
1705
1706
|
});
|
|
1706
1707
|
}
|
|
1707
|
-
// A lead listens while holding no work of their own: a
|
|
1708
|
+
// A lead listens while holding no work of their own: a listening task is the only task.
|
|
1708
1709
|
if (Array.isArray(snapshot.tasks) && snapshot.tasks.length > 1) {
|
|
1709
1710
|
snapshot.tasks.forEach((task, index) => {
|
|
1710
|
-
if (isPlainObject(task) && task.
|
|
1711
|
-
into.add("snapshot.
|
|
1711
|
+
if (isPlainObject(task) && task.listening !== undefined) {
|
|
1712
|
+
into.add("snapshot.listening.alone", `${path}.tasks[${index}].listening`, "a lead listens only while holding no task of their own");
|
|
1712
1713
|
}
|
|
1713
1714
|
});
|
|
1714
1715
|
}
|
|
@@ -2246,7 +2247,7 @@ export function validateHostReport(report, path = "host") {
|
|
|
2246
2247
|
// Commands.
|
|
2247
2248
|
// ---------------------------------------------------------------------------
|
|
2248
2249
|
const TRANSFER_ACTIONS = ["cold", "warm", "complete", "cancel"];
|
|
2249
|
-
const LEAD_ASSIST_ACTIONS = ["request", "cancel", "take-over", "leave"];
|
|
2250
|
+
const LEAD_ASSIST_ACTIONS = ["request", "cancel", "take-over-call", "leave"];
|
|
2250
2251
|
const CONFERENCE_ACTIONS = ["add", "remove"];
|
|
2251
2252
|
/** Validate the exact interaction/allocation target before provider command dispatch. */
|
|
2252
2253
|
export function validateTaskCommandRequest(request, task, path = "request", taskContext) {
|
|
@@ -2553,7 +2554,7 @@ const RESULT_STATUSES = {
|
|
|
2553
2554
|
endBreak: { success: "ended", failure: "failed" },
|
|
2554
2555
|
executeTeamBreak: { success: "applied", failure: "failed" },
|
|
2555
2556
|
executeTeamLeadAssist: { success: "applied", failure: "failed" },
|
|
2556
|
-
|
|
2557
|
+
executeTeamListen: { success: "applied", failure: "failed" },
|
|
2557
2558
|
openMedia: { success: "opened", failure: "unavailable" },
|
|
2558
2559
|
setPreference: { success: "applied", failure: "failed" },
|
|
2559
2560
|
recordStep: { success: "recorded", failure: "failed" },
|
|
@@ -2704,8 +2705,8 @@ function validateUserCapabilitiesInto(value, path, into, levels) {
|
|
|
2704
2705
|
continue;
|
|
2705
2706
|
if (!into.require(TEAM_CAPABILITIES.includes(control), "authentication.capability.team.unknown", `${path}.team.${control}`, `unsupported team capability: ${control}`))
|
|
2706
2707
|
continue;
|
|
2707
|
-
if (control === "
|
|
2708
|
-
|
|
2708
|
+
if (control === "listeningControl") {
|
|
2709
|
+
validateListeningControl(on, `${path}.team.listeningControl`, into);
|
|
2709
2710
|
continue;
|
|
2710
2711
|
}
|
|
2711
2712
|
into.require(on === true, "authentication.capability.value", `${path}.team.${control}`, `${control} is declared by presence: send true or omit it`);
|
|
@@ -2716,20 +2717,20 @@ function validateUserCapabilitiesInto(value, path, into, levels) {
|
|
|
2716
2717
|
}
|
|
2717
2718
|
}
|
|
2718
2719
|
/**
|
|
2719
|
-
* The modes a lead may listen in, as a list: coach and join-call begin
|
|
2720
|
-
* without `
|
|
2720
|
+
* The modes a lead may listen in, as a list: coach and join-call begin with silent listening, so a list
|
|
2721
|
+
* without `listen` names modes the lead could never reach.
|
|
2721
2722
|
*/
|
|
2722
|
-
function
|
|
2723
|
+
function validateListeningControl(value, path, into) {
|
|
2723
2724
|
if (!Array.isArray(value) || value.length === 0) {
|
|
2724
|
-
into.add("authentication.capability.team.
|
|
2725
|
+
into.add("authentication.capability.team.listeningControl.shape", path, "listeningControl lists the modes this lead may listen in: a non-empty array, or omitted for a lead who may not");
|
|
2725
2726
|
return;
|
|
2726
2727
|
}
|
|
2727
2728
|
value.forEach((mode, index) => {
|
|
2728
|
-
if (into.oneOf(mode,
|
|
2729
|
-
into.add("authentication.capability.team.
|
|
2729
|
+
if (into.oneOf(mode, LISTENING_MODES, "authentication.capability.team.listeningControl.mode", `${path}[${index}]`) && value.indexOf(mode) !== index) {
|
|
2730
|
+
into.add("authentication.capability.team.listeningControl.unique", `${path}[${index}]`, `duplicate listen mode: ${describeValue(mode)}`);
|
|
2730
2731
|
}
|
|
2731
2732
|
});
|
|
2732
|
-
into.require(value.includes("
|
|
2733
|
+
into.require(value.includes("listen"), "authentication.capability.team.listeningControl.listen", path, "coach and join-call begin with silent listening: a lead who may listen in any mode may listen");
|
|
2733
2734
|
}
|
|
2734
2735
|
/**
|
|
2735
2736
|
* A failure an authentication reports: on a `rejected` start or completion, or on an `expired`
|
package/guide.md
CHANGED
|
@@ -40,7 +40,7 @@ are used precisely throughout and mean nothing looser here.
|
|
|
40
40
|
| **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. |
|
|
41
41
|
| **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, team member list. |
|
|
42
42
|
| **Dial** | One outbound call the host asks a provider to place — from the idle dialpad, a cold or warm 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**. |
|
|
43
|
-
| **
|
|
43
|
+
| **Listen** | A lead listening to a member's call unasked, from the team member list: `listen` in silence, `coach` heard by the agent alone, `join-call` heard by everyone. Nothing of it reaches the member's task, and there is no take-over in it. See **Listening to a call**. |
|
|
44
44
|
| **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. |
|
|
45
45
|
|
|
46
46
|
Six words describe *what state a thing is in*, and they are not interchangeable: each belongs to
|
|
@@ -246,12 +246,12 @@ type AuthenticationContext = {
|
|
|
246
246
|
log?: (entry: unknown) => void;
|
|
247
247
|
};
|
|
248
248
|
|
|
249
|
-
type
|
|
249
|
+
type ListeningMode = "listen" | "coach" | "join-call";
|
|
250
250
|
|
|
251
251
|
type TeamCapabilities = {
|
|
252
252
|
breakControl?: true;
|
|
253
253
|
leadAssistControl?: true;
|
|
254
|
-
|
|
254
|
+
listeningControl?: ListeningMode[];
|
|
255
255
|
policyControl?: true;
|
|
256
256
|
};
|
|
257
257
|
|
|
@@ -702,11 +702,11 @@ type TaskAssisting = {
|
|
|
702
702
|
since: IsoTimestamp;
|
|
703
703
|
};
|
|
704
704
|
|
|
705
|
-
type
|
|
705
|
+
type TaskListening = {
|
|
706
706
|
memberId: UserId;
|
|
707
707
|
taskId: TaskId;
|
|
708
708
|
allocationId: AllocationId;
|
|
709
|
-
mode:
|
|
709
|
+
mode: ListeningMode;
|
|
710
710
|
since: IsoTimestamp;
|
|
711
711
|
};
|
|
712
712
|
|
|
@@ -747,8 +747,8 @@ type Task<C extends Channel = Channel> = {
|
|
|
747
747
|
interactionHistory?: TaskInteractionHistory;
|
|
748
748
|
} & TaskCompletion & (
|
|
749
749
|
C extends "voice"
|
|
750
|
-
? { recording?: { provider?: RecordingState }; onCall?: OnCall[]; leadAssist?: TaskLeadAssist; assisting?: TaskAssisting;
|
|
751
|
-
: { recording?: never; onCall?: never; leadAssist?: never; assisting?: never;
|
|
750
|
+
? { recording?: { provider?: RecordingState }; onCall?: OnCall[]; leadAssist?: TaskLeadAssist; assisting?: TaskAssisting; listening?: TaskListening; media?: TaskMediaState }
|
|
751
|
+
: { recording?: never; onCall?: never; leadAssist?: never; assisting?: never; listening?: never; media?: never }
|
|
752
752
|
);
|
|
753
753
|
|
|
754
754
|
type PreviewDeadline = "calls" | "host-calls" | "waits";
|
|
@@ -809,7 +809,7 @@ type VoiceTaskCommand =
|
|
|
809
809
|
| { type: "transfer"; action: "cancel" }
|
|
810
810
|
| { type: "lead-assist"; action: "request"; note?: string }
|
|
811
811
|
| { type: "lead-assist"; action: "cancel" }
|
|
812
|
-
| { type: "lead-assist"; action: "take-over" }
|
|
812
|
+
| { type: "lead-assist"; action: "take-over-call" }
|
|
813
813
|
| { type: "lead-assist"; action: "leave" }
|
|
814
814
|
| { type: "conference"; action: "add"; dialId: DialId; destinationId: string }
|
|
815
815
|
| { type: "conference"; action: "remove"; destinationId: string; party?: never }
|
|
@@ -1047,14 +1047,14 @@ type TeamLeadAssistCommandRequest = {
|
|
|
1047
1047
|
command: TeamLeadAssistCommand;
|
|
1048
1048
|
};
|
|
1049
1049
|
|
|
1050
|
-
type
|
|
1051
|
-
| { type: "
|
|
1050
|
+
type TeamListenCommand =
|
|
1051
|
+
| { type: "listen"; memberId: UserId }
|
|
1052
1052
|
| { type: "coach" }
|
|
1053
1053
|
| { type: "join-call" }
|
|
1054
1054
|
| { type: "leave" };
|
|
1055
1055
|
|
|
1056
|
-
type
|
|
1057
|
-
command:
|
|
1056
|
+
type TeamListenCommandRequest = {
|
|
1057
|
+
command: TeamListenCommand;
|
|
1058
1058
|
};
|
|
1059
1059
|
|
|
1060
1060
|
type TeamCommandResult =
|
|
@@ -1173,7 +1173,7 @@ type Connection<C extends Channel = Channel> = {
|
|
|
1173
1173
|
|
|
1174
1174
|
executeTeamBreak?(request: TeamBreakCommandRequest): Promise<TeamCommandResult>;
|
|
1175
1175
|
executeTeamLeadAssist?(request: TeamLeadAssistCommandRequest): Promise<TeamCommandResult>;
|
|
1176
|
-
|
|
1176
|
+
executeTeamListen?(request: TeamListenCommandRequest): Promise<TeamCommandResult>;
|
|
1177
1177
|
openMedia?(request: OpenMediaRequest): Promise<OpenMediaResult>;
|
|
1178
1178
|
setPreference?(request: SetPreferenceRequest): Promise<PreferenceResult>;
|
|
1179
1179
|
recordStep?(report: InteractionReport): Promise<InteractionReportResult>;
|
|
@@ -1613,7 +1613,7 @@ compile time.
|
|
|
1613
1613
|
| `timeCheck` | Optional `true`: implements `checkTime` for fresh provider-clock samples; host polling is independently opt-in. |
|
|
1614
1614
|
| `timestampAuthority` | Optional `"provider"`: provider timestamps are final; host instants are advisory. Omission makes no trust promise. |
|
|
1615
1615
|
| `runningStepReports` | The provider takes running reports of a host-performed step — `recordStep` with `seconds` so far and no `ended`. Omitted, the host sends exactly two reports per leg, when it began and when it ended, and a running one is refused. See **The host records what it performs**. |
|
|
1616
|
-
| `completionSettleMs` | Required. How long after an applied completion -- `complete`, or a lead's `take-over` -- the provider's `task-ended` is owed, a positive whole number of milliseconds (`manifest.completionSettleMs`). A warm transfer's `complete` is not final task completion: the agent's wrap runs after it. Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
|
|
1616
|
+
| `completionSettleMs` | Required. How long after an applied completion -- `complete`, or a lead's `take-over-call` -- the provider's `task-ended` is owed, a positive whole number of milliseconds (`manifest.completionSettleMs`). A warm transfer's `complete` is not final task completion: the agent's wrap runs after it. Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
|
|
1617
1617
|
|
|
1618
1618
|
### Authentication methods
|
|
1619
1619
|
|
|
@@ -1926,7 +1926,7 @@ them from what arrives later.
|
|
|
1926
1926
|
| `team` | This login leads a team. The provider publishes a `TeamMembers` object to it on every snapshot — `members: []` when nobody is in it — and to nobody else. |
|
|
1927
1927
|
| `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`. |
|
|
1928
1928
|
| `team.leadAssistControl` | This lead may join a member's call on request. Requires `executeTeamLeadAssist`. |
|
|
1929
|
-
| `team.
|
|
1929
|
+
| `team.listeningControl` | This lead may listen to a member's call unasked, in the listed modes and no others: `listen`, `coach`, `join-call`. The list always includes `listen`, since the other two begin from one. Requires `executeTeamListen`. See **Listening to a call**. |
|
|
1930
1930
|
| `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 team member list carries `policies`. |
|
|
1931
1931
|
| `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**. |
|
|
1932
1932
|
|
|
@@ -2161,7 +2161,7 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
2161
2161
|
| `endBreak()` | The login declares `capabilities.breaks`. |
|
|
2162
2162
|
| `executeTeamBreak(command)` | The login declares `capabilities.team.breakControl`. |
|
|
2163
2163
|
| `executeTeamLeadAssist(command)` | The login declares `capabilities.team.leadAssistControl`. |
|
|
2164
|
-
| `
|
|
2164
|
+
| `executeTeamListen(command)` | The login declares `capabilities.team.listeningControl`. |
|
|
2165
2165
|
| `setPreference(request)` | The login declares `capabilities.preferences`: the person's choice has to have somewhere to go. |
|
|
2166
2166
|
| `recordStep(report)` | The manifest lists `softphone` among its `phones`. On a softphone the host mutes its own microphone on any call, and the provider's record has to have somewhere to take that leg; a desk phone's microphone is the phone's. See **The host records what it performs**. |
|
|
2167
2167
|
| `executeTeamPolicy(command)` | The login declares `capabilities.team.policyControl`. |
|
|
@@ -2297,7 +2297,7 @@ An automatically accepted task still arrives through `task-offered`.
|
|
|
2297
2297
|
|
|
2298
2298
|
**Work the agent originated is accepted by the command that created it.** A task born of the
|
|
2299
2299
|
agent's own act — a dialpad call or a connect-back, recognisable by the host's `dialId` on
|
|
2300
|
-
`onCall`; a lead's join, carrying `assisting`; a lead's
|
|
2300
|
+
`onCall`; a lead's join, carrying `assisting`; a lead's listen, carrying `listening` — arrives
|
|
2301
2301
|
through `task-offered` with `acceptance: "automatic"` whatever `autoAcceptTasks` says, and the
|
|
2302
2302
|
validator holds it there under either provisioning and under none (`task.acceptance.originated`):
|
|
2303
2303
|
the desk shows no **Accept** for a call the agent placed. The provisioning governs work the queue
|
|
@@ -2394,8 +2394,8 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2394
2394
|
| `interactionHistory` | The call record: `steps` — the ordered interaction history of this open task, one entry per occurrence, oldest first — and what they add up to before this agent, `interactionSeconds`, `holdSeconds`, `queueSeconds`, `transfers`, each present when the provider knows it. Live task data restated with the task, not a permanent archive. See **Interaction history**. |
|
|
2395
2395
|
| `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 -- carrying a `stage` while being dialled again on the same task, a connect-back with the host's `dialId` or a platform's callback without, ringing from the moment the dial is placed and joined on its answered outcome --, `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 `destinationId` 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**. |
|
|
2396
2396
|
| `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**. |
|
|
2397
|
-
| `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**. |
|
|
2398
|
-
| `
|
|
2397
|
+
| `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-call` and `leave` issuable. See **Lead assist**. |
|
|
2398
|
+
| `listening` | 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 **Listening to a call**. |
|
|
2399
2399
|
|
|
2400
2400
|
`TaskAttribute` entries carry typed detail alongside the task:
|
|
2401
2401
|
|
|
@@ -3625,7 +3625,7 @@ rendering one as the other tells an agent to wait for somebody who is never comi
|
|
|
3625
3625
|
| `awaiting-decision` | A person has to decide. The agent is waiting on somebody. |
|
|
3626
3626
|
| `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. |
|
|
3627
3627
|
| `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. It waits on a task, so beside no task it is refused (`break.starting-after-task.tasks`): a committed break with nothing outstanding is `in-effect`. |
|
|
3628
|
-
| `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
|
|
3628
|
+
| `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 listening task during a `coaching`, `administrative` or `training` break -- see **Listening to a call**. |
|
|
3629
3629
|
|
|
3630
3630
|
A denial is a decision, not a standing approval state. The provider transitions the request directly
|
|
3631
3631
|
to `not-requested`; Omni returns the agent to idle and never asks again on their behalf. They saw the
|
|
@@ -3771,7 +3771,7 @@ that association and backend authorization are provider responsibilities.
|
|
|
3771
3771
|
|
|
3772
3772
|
Use `validateBreakStatus(state, tasks)` on the complete retained task view after each transaction,
|
|
3773
3773
|
as well as `validateBreakTransition` for event ordering. Snapshot validation shares the status
|
|
3774
|
-
checks. Pending and completing tasks still count as work. Only the existing
|
|
3774
|
+
checks. Pending and completing tasks still count as work. Only the existing listening exception
|
|
3775
3775
|
allows tasks beside a break in effect. Validate full task shapes and the envelope separately.
|
|
3776
3776
|
|
|
3777
3777
|
These validators report violations without dispatching, rewriting state or proving freshness.
|
|
@@ -4158,12 +4158,19 @@ The request stands for another lead, as it does when this one is already on a ca
|
|
|
4158
4158
|
provider clears `leadAssist` from the agent's task** and drops the request from every team member list. Nothing
|
|
4159
4159
|
else changes; the agent is still on the call.
|
|
4160
4160
|
|
|
4161
|
+
**Take over call** uses `{ type: "lead-assist", action: "take-over-call" }`.
|
|
4162
|
+
The lead assumes the agent’s interaction; the caller’s wider journey continues.
|
|
4163
|
+
Migration: replace the former take-over action literal with `take-over-call` in hosts and
|
|
4164
|
+
providers together. The former spelling is rejected, with no compatibility alias.
|
|
4165
|
+
The `taken-over` task outcome and existing completion behavior remain unchanged.
|
|
4166
|
+
This action is distinct from `join-call`, which lets everyone hear a lead listening to a call.
|
|
4167
|
+
|
|
4161
4168
|
The lead then has two commands on their copy, gated by `assisting` being present, and a third
|
|
4162
4169
|
choice that is no command at all:
|
|
4163
4170
|
|
|
4164
4171
|
| The lead | The agent's task | The lead's task |
|
|
4165
4172
|
| --- | --- | --- |
|
|
4166
|
-
| `{ type: "lead-assist", action: "take-over" }` | `task-media-ended`, then `task-ended` with `{ type: "taken-over", leadId }`: **no `completing` window**, the agent is idle at once. The audio ends first, as before every voice ending (`stream.taskEnded.mediaOpen`), and the lead is named by user id, since a lead is not a directory item | Continues alone, and ends as any call does |
|
|
4173
|
+
| `{ type: "lead-assist", action: "take-over-call" }` | `task-media-ended`, then `task-ended` with `{ type: "taken-over", leadId }`: **no `completing` window**, the agent is idle at once. The audio ends first, as before every voice ending (`stream.taskEnded.mediaOpen`), and the lead is named by user id, since a lead is not a directory item | Continues alone, and ends as any call does |
|
|
4167
4174
|
| `{ type: "lead-assist", action: "leave" }` | Continues; `leadAssist` is cleared | `task-media-ended`, then `task-ended` with `{ type: "left" }` -- the call goes on without them |
|
|
4168
4175
|
| Stays until the customer hangs up | `task-media-ended`, `completing`, its own outcome | The same, independently: **both have the completion window** |
|
|
4169
4176
|
|
|
@@ -4183,20 +4190,40 @@ const leadAssistCapable = {
|
|
|
4183
4190
|
Lead and member alike are `UserId`s of this provider, so an adapter publishing them implements
|
|
4184
4191
|
`describeUsers()`; names never travel on a task or a team member list.
|
|
4185
4192
|
|
|
4186
|
-
###
|
|
4193
|
+
### Listening to a call
|
|
4194
|
+
|
|
4195
|
+
**Listen** lets the lead hear the call without being heard. The modes are `listen`, `coach`
|
|
4196
|
+
(agent-only lead audio), and `join-call` (lead audio heard by everyone).
|
|
4197
|
+
|
|
4198
|
+
| Former API | Current API |
|
|
4199
|
+
| --- | --- |
|
|
4200
|
+
| MonitorMode / MONITOR_MODES | `ListeningMode` / `LISTENING_MODES` |
|
|
4201
|
+
| TeamMonitorCommand / TeamMonitorCommandRequest | `TeamListenCommand` / `TeamListenCommandRequest` |
|
|
4202
|
+
| executeTeamMonitor | `executeTeamListen` |
|
|
4203
|
+
| team.monitorControl | `team.listeningControl` |
|
|
4204
|
+
| TaskMonitoring / task.monitoring | `TaskListening` / the task’s `listening` field |
|
|
4205
|
+
| MONITORING_BREAK_KINDS / breakKindAllowsMonitoring | `LISTENING_BREAK_KINDS` / `breakKindAllowsListening` |
|
|
4206
|
+
| monitor action and mode | `listen` action and mode |
|
|
4207
|
+
|
|
4208
|
+
Update command dispatch, allowed modes, task snapshots, imports and diagnostics together.
|
|
4209
|
+
The former names are not aliases. Old task and permission fields are rejected even alongside
|
|
4210
|
+
new fields. Validation rule segments now use `listening`, `listeningControl`, and `listen`.
|
|
4211
|
+
The existing login permissions, single-call limits, allowed break kinds and audio effects
|
|
4212
|
+
remain unchanged. Listening includes its coach/join-call modes and does not imply takeover.
|
|
4213
|
+
|
|
4187
4214
|
|
|
4188
4215
|
**Coach** lets the lead speak privately to the agent; the caller cannot hear the lead.
|
|
4189
|
-
The command and
|
|
4190
|
-
in `
|
|
4191
|
-
`
|
|
4192
|
-
together. No legacy alias is accepted. The lead must already be
|
|
4216
|
+
The command and listening mode are `coach`. Migration: replace the former whisper literal
|
|
4217
|
+
in `TeamListenCommand`, `ListeningMode`, `team.listeningControl`, and the task’s `listening.mode`.
|
|
4218
|
+
`LISTENING_MODES` exports `coach`; update command producers, permissions and retained snapshots
|
|
4219
|
+
together. No legacy alias is accepted. The lead must already be listening and permitted to
|
|
4193
4220
|
coach. This mode does not join the audible caller conversation or take over the task.
|
|
4194
4221
|
|
|
4195
|
-
The user-facing action is **Join call**. The command and
|
|
4196
|
-
Migration: replace the former barge literal in `
|
|
4197
|
-
`team.
|
|
4222
|
+
The user-facing action is **Join call**. The command and listening mode are `join-call`.
|
|
4223
|
+
Migration: replace the former barge literal in `TeamListenCommand`, `ListeningMode`,
|
|
4224
|
+
`team.listeningControl`, and the task’s `listening.mode`. `LISTENING_MODES` exports the new literal;
|
|
4198
4225
|
no legacy alias is accepted. Update commands, capability declarations and retained snapshots
|
|
4199
|
-
together. The lead must already be
|
|
4226
|
+
together. The lead must already be listening and permitted to use this mode. Everyone on the
|
|
4200
4227
|
call hears the lead; this does not transfer ownership or invoke lead-assist takeover.
|
|
4201
4228
|
|
|
4202
4229
|
A lead may listen to a member's call without being asked -- to coach, to check quality, to step in
|
|
@@ -4206,63 +4233,63 @@ three modes determine who hears the lead:
|
|
|
4206
4233
|
|
|
4207
4234
|
| Mode | Who hears the lead |
|
|
4208
4235
|
| --- | --- |
|
|
4209
|
-
| `
|
|
4236
|
+
| `listen` | Nobody. The lead hears both sides in silence. |
|
|
4210
4237
|
| `coach` | The agent alone. The customer hears nothing. |
|
|
4211
4238
|
| `join-call` | Everyone on the call. |
|
|
4212
4239
|
|
|
4213
|
-
The login says which modes this lead has, as a list on `team.
|
|
4214
|
-
includes `
|
|
4215
|
-
(`authentication.capability.team.
|
|
4216
|
-
reserves joining the call declares `["
|
|
4217
|
-
declaration requires `
|
|
4240
|
+
The login says which modes this lead has, as a list on `team.listeningControl`, and it always
|
|
4241
|
+
includes `listen`, because coach and join-call begin from one
|
|
4242
|
+
(`authentication.capability.team.listeningControl.listen`). A centre that lets every lead listen but
|
|
4243
|
+
reserves joining the call declares `["listen", "coach"]`, and Omni offers no Join call to that lead. The
|
|
4244
|
+
declaration requires `executeTeamListen`, gated exactly as `executeTeamLeadAssist` is by
|
|
4218
4245
|
`team.leadAssistControl`.
|
|
4219
4246
|
|
|
4220
4247
|
```ts
|
|
4221
4248
|
// 1. The lead picks a member from the team member list and starts silent.
|
|
4222
|
-
|
|
4249
|
+
executeTeamListen({ command: { type: "listen", memberId: "A-1" } })
|
|
4223
4250
|
|
|
4224
4251
|
// 2. The lead's own task arrives -- task-offered with `automatic`, as a joined call does -- and
|
|
4225
|
-
// carries `
|
|
4226
|
-
//
|
|
4252
|
+
// carries `listening`; the task id is the lead's, the member's call is named inside it.
|
|
4253
|
+
// listening: { memberId: "A-1", taskId: "call-42", allocationId: "alloc-42", mode: "listen", since }
|
|
4227
4254
|
|
|
4228
4255
|
// 3. The lead changes how they are heard; the provider restates the task with the new mode.
|
|
4229
|
-
|
|
4230
|
-
|
|
4256
|
+
executeTeamListen({ command: { type: "coach" } })
|
|
4257
|
+
executeTeamListen({ command: { type: "join-call" } })
|
|
4231
4258
|
|
|
4232
4259
|
// 4. The lead leaves; their task ends with `{ type: "left" }`. The member's call goes on.
|
|
4233
|
-
|
|
4260
|
+
executeTeamListen({ command: { type: "leave" } })
|
|
4234
4261
|
```
|
|
4235
4262
|
|
|
4236
4263
|
**The lead's task is what gives them audio**: Omni opens media on it as on any voice task, and the
|
|
4237
|
-
provider bridges the lead's leg into the member's call in the mode stated. `
|
|
4264
|
+
provider bridges the lead's leg into the member's call in the mode stated. `listening.mode` is
|
|
4238
4265
|
the state, restated on every change, and a `coach` or `join-call` the login's list does not include
|
|
4239
|
-
is answered `failed`. **A lead listens to one call at a time** (`snapshot.
|
|
4240
|
-
a task carries lead assistance or
|
|
4241
|
-
The `join-call` mode still belongs to
|
|
4266
|
+
is answered `failed`. **A lead listens to one call at a time** (`snapshot.listening.single`), and
|
|
4267
|
+
a task carries lead assistance or listening, never both (`task.listening.assisting`).
|
|
4268
|
+
The `join-call` mode still belongs to listening; it does not create `assisting`. The member's
|
|
4242
4269
|
call ending ends the lead's task as it ends the member's, with `left`, since the lead was never
|
|
4243
4270
|
working on it.
|
|
4244
4271
|
|
|
4245
|
-
**A lead listens only while holding no work of their own.** A
|
|
4246
|
-
task, and a snapshot carrying one beside any other is refused (`snapshot.
|
|
4247
|
-
offers
|
|
4272
|
+
**A lead listens only while holding no work of their own.** A listening task is the lead's only
|
|
4273
|
+
task, and a snapshot carrying one beside any other is refused (`snapshot.listening.alone`); Omni
|
|
4274
|
+
offers Listen to a lead with no task. That includes a lead on a break -- unlike joining, which a
|
|
4248
4275
|
break forbids -- but only a break that is work of another sort: `coaching`, `administrative` or
|
|
4249
|
-
`training`, the kinds in `
|
|
4250
|
-
otherwise holds no task; the
|
|
4276
|
+
`training`, the kinds in `LISTENING_BREAK_KINDS`, and never a meal or a rest. A break in effect
|
|
4277
|
+
otherwise holds no task; the listening task is its one exception, and a snapshot carrying one
|
|
4251
4278
|
during a break whose active reason is of any other kind, or of no stated kind, is refused
|
|
4252
|
-
(`snapshot.
|
|
4279
|
+
(`snapshot.listening.break`). The kind decides, not the reason's name: a centre that wants leads
|
|
4253
4280
|
listening during a break declares that break `coaching`, `administrative` or `training`.
|
|
4254
4281
|
|
|
4255
|
-
**Nothing reaches the member.** The member's task carries no trace of a
|
|
4282
|
+
**Nothing reaches the member.** The member's task carries no trace of a listening lead in any
|
|
4256
4283
|
mode: not on `onCall`, not in the record. Whether coaching is announced to the agent is the
|
|
4257
4284
|
platform's business and travels on the audio, not on this wire.
|
|
4258
4285
|
|
|
4259
4286
|
```ts
|
|
4260
|
-
const
|
|
4287
|
+
const listeningLead = {
|
|
4261
4288
|
channel: "voice",
|
|
4262
4289
|
capabilities: {},
|
|
4263
4290
|
phase: "in-progress",
|
|
4264
|
-
|
|
4265
|
-
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "
|
|
4291
|
+
listening: { memberId: "A-1", taskId: "call-42", allocationId: "alloc-42", mode: "coach", since: "2026-08-21T09:04:00Z" },
|
|
4292
|
+
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "listening">;
|
|
4266
4293
|
```
|
|
4267
4294
|
|
|
4268
4295
|
### A member waiting for a break
|
|
@@ -4621,7 +4648,7 @@ declared:
|
|
|
4621
4648
|
| `transfer` with `action: "warm"` | The `warmTransfer` capability. `action: "cold"` is gated by `coldTransfer`; the two are declared and offered separately. |
|
|
4622
4649
|
| `transfer` with `action: "complete"` or `"cancel"` | A consultation in progress -- a `consulted` entry on `Task.onCall`. Without one there is nothing to complete or cancel, and a provider that receives either answers `failed`. |
|
|
4623
4650
|
| `lead-assist` with `action: "request"` or `"cancel"` | The `leadAssist` capability. `cancel` needs a request standing -- `Task.leadAssist` with status `requested`. |
|
|
4624
|
-
| `lead-assist` with `action: "take-over"` or `"leave"` | The lead's own task, on a call they joined -- `Task.assisting` present. An agent's task never has it, and a provider that receives either without it answers `failed`. |
|
|
4651
|
+
| `lead-assist` with `action: "take-over-call"` or `"leave"` | The lead's own task, on a call they joined -- `Task.assisting` present. An agent's task never has it, and a provider that receives either without it answers `failed`. |
|
|
4625
4652
|
| `transfer` with a destination, `conference` with `action: "add"` | Its capability, and a `destinationId` the directory offered: the id Omni sends is the id the provider published (`command.destination.unknown`). |
|
|
4626
4653
|
| `custom` | A control the task published under `capabilities.custom`, by its `id` (`command.capability.custom`), carrying a non-empty string for each `required` prompt field and strings for any optional fields supplied (`command.custom.prompt`). A toggle carries its target `on` boolean (`command.custom.on`). |
|
|
4627
4654
|
| Everything else | Its own named capability. |
|
|
@@ -5024,7 +5051,7 @@ A `left` outcome ends the task for this agent alone: the call continues without
|
|
|
5024
5051
|
when a lead who joined it leaves -- see **Lead assist**.
|
|
5025
5052
|
|
|
5026
5053
|
A successful `complete` or `transfer` command does not clear the task. Omni waits for `task-ended`,
|
|
5027
|
-
and not for ever: `applied` to a completion -- `complete`, or a lead's `take-over` -- says the
|
|
5054
|
+
and not for ever: `applied` to a completion -- `complete`, or a lead's `take-over-call` -- says the
|
|
5028
5055
|
provider has completed the task, and its `task-ended` follows within the
|
|
5029
5056
|
manifest's `completionSettleMs`. A provider never answers `applied` for a completion it has not yet
|
|
5030
5057
|
performed. Past the bound the host calls `snapshot()`: a snapshot still carrying the task is a task
|