@xema/omni-protocol 0.1.100 → 0.1.101
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 +30 -17
- package/dist/index.js +1 -0
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +4 -1
- package/dist/validation.js +45 -9
- package/guide.md +75 -41
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -866,7 +866,7 @@ export type TaskLeadAssist = {
|
|
|
866
866
|
* On a task that reached this agent by a lead's take-over: which member the call was taken from,
|
|
867
867
|
* and when. The provider moves the call from one agent to another, and the lead is the new agent,
|
|
868
868
|
* so the task is an ordinary assignment with the call's history -- offered whatever break the lead
|
|
869
|
-
* is on, counted against
|
|
869
|
+
* is on, counted against their capacity like any other call, provided they are under it and their voice channel is free.
|
|
870
870
|
*/
|
|
871
871
|
export interface TaskTakenOver {
|
|
872
872
|
memberId: UserId;
|
|
@@ -1041,7 +1041,7 @@ export type VoiceTaskCommand = {
|
|
|
1041
1041
|
| {
|
|
1042
1042
|
type: "end-call";
|
|
1043
1043
|
}
|
|
1044
|
-
/** End the caller
|
|
1044
|
+
/** End the whole call at the provider: every channel on it, the agent's, the caller's and anyone else's. Gated by `terminateCall`. */
|
|
1045
1045
|
| {
|
|
1046
1046
|
type: "terminate-call";
|
|
1047
1047
|
}
|
|
@@ -1269,6 +1269,8 @@ export interface TeamMember {
|
|
|
1269
1269
|
tasks?: MemberTask[];
|
|
1270
1270
|
/** Present while this lead is on the member's call, listening, coaching or joined; absent otherwise. */
|
|
1271
1271
|
listening?: MemberListening;
|
|
1272
|
+
/** The member's own history for the day: sign-in, sign-out, breaks and productivity. Omitted where the provider cannot say. */
|
|
1273
|
+
shift?: MemberShift;
|
|
1272
1274
|
}
|
|
1273
1275
|
/** A member asking this lead to join their call, named by the member and the assignment. */
|
|
1274
1276
|
export interface LeadRequest {
|
|
@@ -1309,11 +1311,36 @@ export type MemberTask<C extends Channel = Channel> = {
|
|
|
1309
1311
|
takenOver?: never;
|
|
1310
1312
|
audio?: never;
|
|
1311
1313
|
});
|
|
1312
|
-
/**
|
|
1314
|
+
/**
|
|
1315
|
+
* The lead on this member's call: which of the member's calls, in the mode they are heard, since
|
|
1316
|
+
* when. Its appearance on the team member list is the provider's word that the lead's audio is up,
|
|
1317
|
+
* and the application opens `openAudio` on the assignment named; its disappearance closes it.
|
|
1318
|
+
*/
|
|
1313
1319
|
export interface MemberListening {
|
|
1320
|
+
assignmentId: AssignmentId;
|
|
1314
1321
|
mode: ListeningMode;
|
|
1315
1322
|
since: IsoTimestamp;
|
|
1316
1323
|
}
|
|
1324
|
+
export type ShiftEventKind = "signed-in" | "signed-out" | "break-started" | "break-ended";
|
|
1325
|
+
export declare const SHIFT_EVENT_KINDS: readonly ["signed-in", "signed-out", "break-started", "break-ended"];
|
|
1326
|
+
export interface ShiftEvent {
|
|
1327
|
+
at: IsoTimestamp;
|
|
1328
|
+
kind: ShiftEventKind;
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* The member's own history for the day, about the person rather than any one call: when they
|
|
1332
|
+
* signed in and out, today's totals as the provider counts them, each present only when the
|
|
1333
|
+
* provider knows it, and the day's sign-in, sign-out and break events, oldest first.
|
|
1334
|
+
*/
|
|
1335
|
+
export interface MemberShift {
|
|
1336
|
+
signedInAt: IsoTimestamp;
|
|
1337
|
+
signedOutAt?: IsoTimestamp;
|
|
1338
|
+
talkSeconds?: DurationSeconds;
|
|
1339
|
+
holdSeconds?: DurationSeconds;
|
|
1340
|
+
breakSeconds?: DurationSeconds;
|
|
1341
|
+
tasksHandled?: number;
|
|
1342
|
+
events?: ShiftEvent[];
|
|
1343
|
+
}
|
|
1317
1344
|
/**
|
|
1318
1345
|
* The lead's view of their team: published to a login that declares `capabilities.lead` while the
|
|
1319
1346
|
* lead has the team feature on, on every snapshot, and to nobody else. The provider keeps it
|
|
@@ -1588,20 +1615,6 @@ export type ProviderEvent<C extends Channel = Channel> = {
|
|
|
1588
1615
|
} | {
|
|
1589
1616
|
type: "task-audio-ended";
|
|
1590
1617
|
assignmentId: AssignmentId;
|
|
1591
|
-
}
|
|
1592
|
-
/**
|
|
1593
|
-
* The lead's channel is in a member's call -- listening, coaching or joined -- and its audio should
|
|
1594
|
-
* attach: on a softphone the application opens audio on the member's assignment as it does for a
|
|
1595
|
-
* task. Voice, to a login that declares `lead`, never to the member.
|
|
1596
|
-
*/
|
|
1597
|
-
| {
|
|
1598
|
-
type: "team-audio-started";
|
|
1599
|
-
memberId: UserId;
|
|
1600
|
-
assignmentId: AssignmentId;
|
|
1601
|
-
} | {
|
|
1602
|
-
type: "team-audio-ended";
|
|
1603
|
-
memberId: UserId;
|
|
1604
|
-
assignmentId: AssignmentId;
|
|
1605
1618
|
} | {
|
|
1606
1619
|
type: "task-ended";
|
|
1607
1620
|
assignmentId: AssignmentId;
|
package/dist/index.js
CHANGED
|
@@ -95,6 +95,7 @@ export const BREAK_KINDS = [
|
|
|
95
95
|
"short-break", "meal", "rest", "training", "coaching",
|
|
96
96
|
"meeting", "administrative", "technical", "personal", "other",
|
|
97
97
|
];
|
|
98
|
+
export const SHIFT_EVENT_KINDS = ["signed-in", "signed-out", "break-started", "break-ended"];
|
|
98
99
|
export const OMNI_FAILURE_CODES = [
|
|
99
100
|
"omni.not-authenticated",
|
|
100
101
|
"omni.capability-not-enabled",
|
package/dist/testing.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation }
|
|
|
7
7
|
* rules -- each optional part of a task, each optional part of the break state and team member list, each
|
|
8
8
|
* declared contribution, and each event type.
|
|
9
9
|
*/
|
|
10
|
-
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.history", "task.onCall", "task.leadAssist", "task.takenOver", "task.audio", "task.acceptance", "task.outcomes", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.forced", "team.members", "team.requests", "team.tasks", "team.listening", "contacts", "calendar", "team.policies"];
|
|
10
|
+
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.history", "task.onCall", "task.leadAssist", "task.takenOver", "task.audio", "task.acceptance", "task.outcomes", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.forced", "team.members", "team.requests", "team.tasks", "team.listening", "team.shift", "contacts", "calendar", "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
|
@@ -27,6 +27,7 @@ const STATE_SUBJECTS = [
|
|
|
27
27
|
"team.requests",
|
|
28
28
|
"team.tasks",
|
|
29
29
|
"team.listening",
|
|
30
|
+
"team.shift",
|
|
30
31
|
"contacts",
|
|
31
32
|
"calendar",
|
|
32
33
|
"team.policies",
|
|
@@ -36,7 +37,7 @@ const STATE_SUBJECTS = [
|
|
|
36
37
|
const EVENT_TYPES = {
|
|
37
38
|
snapshot: true, "transport-status": true, "break-state": true, "task-offered": true, "task-updated": true,
|
|
38
39
|
"task-audio-started": true, "task-audio-ended": true, "task-ended": true, "dial-outcome": true, announcement: true, "queue-summary": true, diagnostic: true,
|
|
39
|
-
"team-updated": true, "
|
|
40
|
+
"team-updated": true, "contacts-updated": true, "calendar-updated": true,
|
|
40
41
|
};
|
|
41
42
|
const CONTRACT_SUBJECTS = [
|
|
42
43
|
...STATE_SUBJECTS,
|
|
@@ -102,6 +103,8 @@ function observeTeam(value, seen) {
|
|
|
102
103
|
seen.add("team.tasks");
|
|
103
104
|
if (member.listening !== undefined)
|
|
104
105
|
seen.add("team.listening");
|
|
106
|
+
if (member.shift !== undefined)
|
|
107
|
+
seen.add("team.shift");
|
|
105
108
|
}
|
|
106
109
|
if (isRecord(value.policies) && Object.keys(value.policies).length > 0)
|
|
107
110
|
seen.add("team.policies");
|
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, BROWSER_ISOLATION_SCHEMES, HISTORY_STEPS_THAT_DIAL, IDLE_CAPABILITIES, OMNI_FAILURE_CODES, OMNI_SUPPORTED_PROTOCOL_VERSIONS, TASK_COMMAND_NAMES, negotiateProtocolVersion, DEFAULT_LEVELS, effectiveLevels, } from "./index.js";
|
|
12
|
+
import { RECORDING_ACTIONS, SHIFT_EVENT_KINDS, ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, BROWSER_ISOLATION_SCHEMES, HISTORY_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") {
|
|
@@ -1671,10 +1671,17 @@ function validateTeamMembersInto(teamMembers, path, context, into) {
|
|
|
1671
1671
|
into.add("team.member.listening.shape", `${at}.listening`, "listening must be an object when present");
|
|
1672
1672
|
}
|
|
1673
1673
|
else {
|
|
1674
|
+
const listened = member.listening;
|
|
1675
|
+
if (into.require(isAssignmentId(member.listening.assignmentId), "team.member.listening.assignmentId", `${at}.listening.assignmentId`, "listening names which of the member's calls the lead is on: the application opens the lead's audio on it") && Array.isArray(member.tasks)) {
|
|
1676
|
+
into.require(member.tasks.some((task) => isPlainObject(task) && task.assignmentId === listened.assignmentId), "team.member.listening.assignment", `${at}.listening.assignmentId`, "the call the lead is on is one the member's tasks carry");
|
|
1677
|
+
}
|
|
1674
1678
|
into.oneOf(member.listening.mode, LISTENING_MODES, "team.member.listening.mode", `${at}.listening.mode`);
|
|
1675
1679
|
into.timestamp(member.listening.since, "team.member.listening.since", `${at}.listening.since`);
|
|
1676
1680
|
}
|
|
1677
1681
|
}
|
|
1682
|
+
// The member's own history for the day: about the person, not any one call.
|
|
1683
|
+
if (member.shift !== undefined)
|
|
1684
|
+
validateShiftInto(member.shift, `${at}.shift`, into);
|
|
1678
1685
|
});
|
|
1679
1686
|
// One member's call at a time.
|
|
1680
1687
|
const listened = teamMembers.members.filter((member) => isPlainObject(member) && member.listening !== undefined);
|
|
@@ -1953,14 +1960,6 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
|
|
|
1953
1960
|
into.require(context.leadFeatures !== false, "event.team.features", `${at}.team`, "the lead turned the team feature off, so nothing of the team reaches them");
|
|
1954
1961
|
validateTeamMembersInto(event.team, `${at}.team`, { ...context, levels, channel: isChannel(channel) ? channel : undefined }, into);
|
|
1955
1962
|
break;
|
|
1956
|
-
case "team-audio-started":
|
|
1957
|
-
case "team-audio-ended":
|
|
1958
|
-
// The lead's channel in a member's call: audio the application opens on the member's assignment.
|
|
1959
|
-
into.require(channel === "voice", "event.audio.channel", `${at}.type`, "only a voice provider publishes audio transitions");
|
|
1960
|
-
into.require(context.capabilities === undefined || context.capabilities.lead === true, "event.teamAudio.capability", `${at}.type`, "team audio reaches a login that declares lead, and nobody else");
|
|
1961
|
-
into.require(isUserId(event.memberId), "event.teamAudio.memberId", `${at}.memberId`, "team audio names the member whose call the lead is on");
|
|
1962
|
-
into.require(isAssignmentId(event.assignmentId), "event.teamAudio.assignmentId", `${at}.assignmentId`, "team audio names the member's assignment");
|
|
1963
|
-
break;
|
|
1964
1963
|
case "contacts-updated":
|
|
1965
1964
|
into.require(idle.contacts === true, "event.contacts.capability", `${at}.contacts`, "contacts-updated requires the contacts idle capability");
|
|
1966
1965
|
if (!Array.isArray(event.contacts)) {
|
|
@@ -2021,6 +2020,43 @@ function validatePreferencesInto(value, path, into, levels) {
|
|
|
2021
2020
|
});
|
|
2022
2021
|
}
|
|
2023
2022
|
/** The team's policy per capability as the lead sees it: the setting, who set it, who locked it. */
|
|
2023
|
+
function validateShiftInto(value, path, into) {
|
|
2024
|
+
if (!isPlainObject(value)) {
|
|
2025
|
+
into.add("team.member.shift.shape", path, "shift must be an object when present");
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
const signedIn = into.timestamp(value.signedInAt, "team.member.shift.signedInAt", `${path}.signedInAt`);
|
|
2029
|
+
if (value.signedOutAt !== undefined && into.timestamp(value.signedOutAt, "team.member.shift.signedOutAt", `${path}.signedOutAt`) && signedIn) {
|
|
2030
|
+
into.require(Date.parse(value.signedOutAt) >= Date.parse(value.signedInAt), "team.member.shift.signedOutAt.order", `${path}.signedOutAt`, "a member signs out after signing in");
|
|
2031
|
+
}
|
|
2032
|
+
for (const field of ["talkSeconds", "holdSeconds", "breakSeconds"]) {
|
|
2033
|
+
if (value[field] !== undefined)
|
|
2034
|
+
into.require(isDurationSeconds(value[field]), `team.member.shift.${field}`, `${path}.${field}`, `${field} must be a whole number of seconds, zero or more, or omitted when unknown`);
|
|
2035
|
+
}
|
|
2036
|
+
if (value.tasksHandled !== undefined)
|
|
2037
|
+
into.require(Number.isInteger(value.tasksHandled) && value.tasksHandled >= 0, "team.member.shift.tasksHandled", `${path}.tasksHandled`, "tasksHandled must be a whole number, zero or more, or omitted when unknown");
|
|
2038
|
+
if (value.events !== undefined) {
|
|
2039
|
+
if (!Array.isArray(value.events)) {
|
|
2040
|
+
into.add("team.member.shift.events.shape", `${path}.events`, "events must be an array when present");
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
let previous;
|
|
2044
|
+
value.events.forEach((event, index) => {
|
|
2045
|
+
const at = `${path}.events[${index}]`;
|
|
2046
|
+
if (!isPlainObject(event)) {
|
|
2047
|
+
into.add("team.member.shift.event.shape", at, "each shift event must be an object");
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
into.oneOf(event.kind, SHIFT_EVENT_KINDS, "team.member.shift.event.kind", `${at}.kind`);
|
|
2051
|
+
if (into.timestamp(event.at, "team.member.shift.event.at", `${at}.at`)) {
|
|
2052
|
+
const instant = Date.parse(event.at);
|
|
2053
|
+
if (previous !== undefined && instant < previous)
|
|
2054
|
+
into.add("team.member.shift.events.order", `${at}.at`, "shift events are oldest first; this event is earlier than the one before it");
|
|
2055
|
+
previous = instant;
|
|
2056
|
+
}
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2024
2060
|
function validateTeamPoliciesInto(value, path, into, levels) {
|
|
2025
2061
|
if (!isPlainObject(value)) {
|
|
2026
2062
|
into.add("team.policies.shape", path, "policies must be an object keyed by capability");
|
package/guide.md
CHANGED
|
@@ -977,6 +977,7 @@ type TeamMember = {
|
|
|
977
977
|
break?: Extract<BreakStatus, "awaiting-approval" | "granted" | "starting-after-task">;
|
|
978
978
|
tasks?: MemberTask[];
|
|
979
979
|
listening?: MemberListening;
|
|
980
|
+
shift?: MemberShift;
|
|
980
981
|
};
|
|
981
982
|
|
|
982
983
|
type LeadRequest = {
|
|
@@ -1006,10 +1007,28 @@ type MemberTask<C extends Channel = Channel> = {
|
|
|
1006
1007
|
: { onCall?: never; leadAssist?: never; takenOver?: never; audio?: never });
|
|
1007
1008
|
|
|
1008
1009
|
type MemberListening = {
|
|
1010
|
+
assignmentId: AssignmentId;
|
|
1009
1011
|
mode: ListeningMode;
|
|
1010
1012
|
since: IsoTimestamp;
|
|
1011
1013
|
};
|
|
1012
1014
|
|
|
1015
|
+
type ShiftEventKind = "signed-in" | "signed-out" | "break-started" | "break-ended";
|
|
1016
|
+
|
|
1017
|
+
type ShiftEvent = {
|
|
1018
|
+
at: IsoTimestamp;
|
|
1019
|
+
kind: ShiftEventKind;
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
type MemberShift = {
|
|
1023
|
+
signedInAt: IsoTimestamp;
|
|
1024
|
+
signedOutAt?: IsoTimestamp;
|
|
1025
|
+
talkSeconds?: DurationSeconds;
|
|
1026
|
+
holdSeconds?: DurationSeconds;
|
|
1027
|
+
breakSeconds?: DurationSeconds;
|
|
1028
|
+
tasksHandled?: number;
|
|
1029
|
+
events?: ShiftEvent[];
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1013
1032
|
type TeamMembers = {
|
|
1014
1033
|
members: TeamMember[];
|
|
1015
1034
|
requests: LeadRequest[];
|
|
@@ -1108,8 +1127,6 @@ type ProviderEvent =
|
|
|
1108
1127
|
| { type: "task-updated"; task: Task }
|
|
1109
1128
|
| { type: "task-audio-started"; assignmentId: AssignmentId }
|
|
1110
1129
|
| { type: "task-audio-ended"; assignmentId: AssignmentId }
|
|
1111
|
-
| { type: "team-audio-started"; memberId: UserId; assignmentId: AssignmentId }
|
|
1112
|
-
| { type: "team-audio-ended"; memberId: UserId; assignmentId: AssignmentId }
|
|
1113
1130
|
| { type: "task-ended"; assignmentId: AssignmentId; outcome: TaskOutcome }
|
|
1114
1131
|
| { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome; assignmentId?: AssignmentId; destinationId?: string; reason?: string }
|
|
1115
1132
|
| { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
|
|
@@ -2382,7 +2399,7 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2382
2399
|
| `history` | 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**. |
|
|
2383
2400
|
| `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 agent application'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 an agent application 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 agent application supplies the verb. Present when the provider knows the room, absent when it does not. See **Every dial has an outcome**. |
|
|
2384
2401
|
| `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**. |
|
|
2385
|
-
| `takenOver` | Voice only, on a task that reached this agent by a lead's take-over: which member the call was taken from, and when. It is an ordinary assignment with the call's history, offered whatever break the lead is on and counted against
|
|
2402
|
+
| `takenOver` | Voice only, on a task that reached this agent by a lead's take-over: which member the call was taken from, and when. It is an ordinary assignment with the call's history, offered whatever break the lead is on and counted against their capacity like any other call. See **Lead assist**. |
|
|
2386
2403
|
|
|
2387
2404
|
`TaskAttribute` entries carry typed detail alongside the task:
|
|
2388
2405
|
|
|
@@ -2730,7 +2747,7 @@ Migration from the earlier spellings:
|
|
|
2730
2747
|
| taskKey, omni.task-not-found, PROVIDER_NAME__TASK_ID__TAB_NAME (ProviderName.TaskId.TabName) | `assignmentKey`, `omni.assignment-not-found`, `PROVIDER_NAME__ASSIGNMENT_ID__TAB_NAME` (`ProviderName.AssignmentId.TabName`) |
|
|
2731
2748
|
| Manifest.disposalSettleMs | `Manifest.completionSettleMs` |
|
|
2732
2749
|
| the call command, atDeadline calls and host-calls | `dial`, `"provider-dials"`, `"host-dials"` |
|
|
2733
|
-
| media: task-media-started/-ended, team-media-*, Task.media, TaskMediaState, openMedia, OpenMediaRequest/Result, VoiceMediaSession and its session field | audio: `task-audio-started`, `task-audio-ended`, `
|
|
2750
|
+
| media: task-media-started/-ended, team-media-*, Task.media, TaskMediaState, openMedia, OpenMediaRequest/Result, VoiceMediaSession and its session field | audio: `task-audio-started`, `task-audio-ended`, `audio` on the task, `TaskAudioState`, `openAudio`, `OpenAudioRequest`, `OpenAudioResult`, `CallAudio` on the result's `audio`; there is no team audio, the lead's follows `listening` on the member |
|
|
2734
2751
|
| capabilitySource values ungoverned and undetermined | `nobody`, `not-yet-read` (`capabilitySource.notYetRead`) |
|
|
2735
2752
|
| Snapshot.scheduledActivities, and scheduledActivities on calendar-updated | `calendar` in both |
|
|
2736
2753
|
| availability reserved | `elsewhere` |
|
|
@@ -3194,7 +3211,7 @@ See **Which commands need a capability**.
|
|
|
3194
3211
|
| `decline` | Pending-task button: Decline | The provider can decline a pending voice offer. Omni shows it only when local policy also permits declining. |
|
|
3195
3212
|
| `hold` | Primary toggle: Hold | Omni may issue voice-task `hold` and `resume` commands. |
|
|
3196
3213
|
| `endCall` | Primary button: End call | The provider ends the agent's channel and every channel the agent added; the caller continues on the provider's path. The task goes to its wrap. See **Ending a call, and removing one person from it**. |
|
|
3197
|
-
| `terminateCall` | Primary button: Terminate call | The provider ends the caller
|
|
3214
|
+
| `terminateCall` | Primary button: Terminate call | The provider ends the whole call: every channel on it, the agent's, the caller's, any colleague's and anyone else's. The task goes to its wrap. See **Ending a call, and removing one person from it**. |
|
|
3198
3215
|
| `connectBack` | Completing-task button: Connect back | Omni may have the provider connect the agent back to the task's party while the task is `completing`, returning it to `in-progress` on the same task. Not offered where there is no `completing` window: `provider-automatic` with a zero allowance completes the task at provider end. See **Connecting back during completion**. |
|
|
3199
3216
|
| `coldTransfer` | Secondary menu item: Cold transfer | Omni may hand the customer straight to a destination, with nobody spoken to first. |
|
|
3200
3217
|
| `warmTransfer` | Secondary menu item: Warm transfer | Omni may park the customer and call a destination first, then hand the customer over or cancel back. See **Warm transfer**. |
|
|
@@ -3294,7 +3311,7 @@ provider. Two of them end the agent's part; they differ in what happens to the c
|
|
|
3294
3311
|
|
|
3295
3312
|
```ts
|
|
3296
3313
|
{ type: "end-call" } // the agent's channel and the channels the agent added end; the caller continues
|
|
3297
|
-
{ type: "terminate-call" } // the
|
|
3314
|
+
{ type: "terminate-call" } // every channel on the call ends
|
|
3298
3315
|
{ type: "conference", action: "remove", party: true } // the customer leaves; the agent stays with the colleague
|
|
3299
3316
|
{ type: "conference", action: "remove", destinationId: "tier2" } // the conferenced person leaves; ringing, this calls the dial off
|
|
3300
3317
|
```
|
|
@@ -3303,9 +3320,10 @@ provider. Two of them end the agent's part; they differ in what happens to the c
|
|
|
3303
3320
|
conferenced colleague, a consulted destination -- end, and the caller continues on the path the
|
|
3304
3321
|
provider decides: another IVR, a queue, a survey, or the end of the call. Gated by `endCall`.
|
|
3305
3322
|
|
|
3306
|
-
**`terminate-call` ends the
|
|
3307
|
-
|
|
3308
|
-
|
|
3323
|
+
**`terminate-call` ends the whole call.** Every channel on it ends at the provider: the agent's,
|
|
3324
|
+
the caller's, any colleague the agent added, and anyone else on the call. Nobody is left on the
|
|
3325
|
+
line. Gated by `terminateCall`, a separate permission, since ending a customer's call is a
|
|
3326
|
+
different thing from putting my own side down.
|
|
3309
3327
|
|
|
3310
3328
|
Both are commands to the provider through `execute`, and nothing happens on the desk until the
|
|
3311
3329
|
provider reports it: the agent's audio ends on `task-audio-ended`, the task moves to `completing`,
|
|
@@ -4131,8 +4149,9 @@ for nothing and computes nothing.
|
|
|
4131
4149
|
| `availability` | Required. What the member is doing now. |
|
|
4132
4150
|
| `since` | Optional. When the current `availability` began — not when they signed in, and not when the team member list was read. |
|
|
4133
4151
|
| `break` | Present only while the member has an outstanding break request. See **A member waiting for a break**. |
|
|
4134
|
-
| `tasks` | The member's open tasks as `MemberTask`s: the same task the member's desk holds, trimmed by the provider. The assignment, title, task type, phase, party, room
|
|
4135
|
-
| `listening` | Present while this lead's channel is in this member's call — listening, coaching or joined —
|
|
4152
|
+
| `tasks` | The member's open tasks as `MemberTask`s: the same task the member's desk holds, trimmed by the provider. The assignment, title, task type, phase, party, room and audio are what the lead reads; the task's history is carried in the same shape as on the member's desk, as much of it as the provider chooses to send; the workspace — controls, their source, browsers, completion terms — is the member's and travels only if the provider sends it. `[]` when the member holds none; omitted only where the provider cannot see them. |
|
|
4153
|
+
| `listening` | Present while this lead's channel is in this member's call — listening, coaching or joined — naming which of the member's calls by `assignmentId`, the `mode` they are heard in, and since when. Its appearance is what the lead's audio opens on. One member's call at a time (`team.listening.single`). |
|
|
4154
|
+
| `shift` | The member's own history for the day, about the person rather than any one call: `signedInAt`, `signedOutAt` once it has happened, today's totals as the provider counts them — `talkSeconds`, `holdSeconds`, `breakSeconds`, `tasksHandled`, each present only when the provider knows it — and the day's sign-in, sign-out and break `events`, oldest first. Omitted where the provider cannot say. |
|
|
4136
4155
|
|
|
4137
4156
|
Each availability value means one thing:
|
|
4138
4157
|
|
|
@@ -4239,8 +4258,8 @@ executeTeam({ command: { type: "decline", memberId: "A-1", reason: "In a call" }
|
|
|
4239
4258
|
|
|
4240
4259
|
**On `join` the provider bridges the lead's channel into the call.** The member's task moves to
|
|
4241
4260
|
`leadAssist: { stage: "joined", leadId }`, the member on the team member list carries
|
|
4242
|
-
`listening: { mode: "join-call", since }`, and the lead's audio
|
|
4243
|
-
describes. Nothing is a task on the lead's desk: a join is the lead's act on the team surface, not
|
|
4261
|
+
`listening: { assignmentId, mode: "join-call", since }`, and the lead's audio opens on it as
|
|
4262
|
+
**Listening to a call** describes. Nothing is a task on the lead's desk: a join is the lead's act on the team surface, not
|
|
4244
4263
|
work assigned to them.
|
|
4245
4264
|
|
|
4246
4265
|
**A lead is on one member's call at a time.** A `join` from a lead already on a call -- their own
|
|
@@ -4261,15 +4280,17 @@ listening unasked -- and one act at the end of either. What follows is the same
|
|
|
4261
4280
|
|
|
4262
4281
|
| | The member's task | The lead's desk |
|
|
4263
4282
|
| --- | --- | --- |
|
|
4264
|
-
| Take-over | `task-audio-ended`, then `completing`: the member's interaction is over and their wrap runs, exactly as after `end-call`. The audio ends first, as before every voice ending (`stream.taskEnded.audioOpen`). | An ordinary assignment arrives: `task-offered` with `acceptance: "automatic"`, carrying the call's history and `takenOver: { memberId, since }
|
|
4283
|
+
| Take-over | `task-audio-ended`, then `completing`: the member's interaction is over and their wrap runs, exactly as after `end-call`. The audio ends first, as before every voice ending (`stream.taskEnded.audioOpen`). The member loses `listening` on the team member list no later than the offer, and the lead's listening audio closes. | An ordinary assignment arrives: `task-offered` with `acceptance: "automatic"`, carrying the call's history and `takenOver: { memberId, since }`, within the lead's capacity. |
|
|
4265
4284
|
| The member completes | `task-ended` with `{ type: "taken-over", leadId }`, at the member's own completion. The lead is named by user id, since a lead is not a directory item. | The lead works the call as any agent would, and ends it as any call ends. |
|
|
4266
4285
|
|
|
4267
|
-
**The taken-over call is offered whatever break the lead is on
|
|
4268
|
-
on a break of a working kind, and the provider assigns the call
|
|
4269
|
-
break in effect may hold (`break.on-break.tasks` allows it, and
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4286
|
+
**The taken-over call is offered whatever break the lead is on, and counted like any other.** A
|
|
4287
|
+
lead working as lead alone is on a break of a working kind, and the provider assigns the call
|
|
4288
|
+
regardless: it is the one task a break in effect may hold (`break.on-break.tasks` allows it, and
|
|
4289
|
+
nothing else). It is a call landing on the lead's desk, so it counts against their capacity as
|
|
4290
|
+
every call does, with no exemption: a lead at capacity cannot take over, and the provider answers
|
|
4291
|
+
`failed`. The other conditions are that the lead is not already on another call and their voice
|
|
4292
|
+
channel is free. A lead with the team feature off cannot take over, since they have no member's
|
|
4293
|
+
assignment to name.
|
|
4273
4294
|
|
|
4274
4295
|
```ts
|
|
4275
4296
|
const leadAssistCapable = {
|
|
@@ -4303,34 +4324,47 @@ history. The three modes determine who hears the lead:
|
|
|
4303
4324
|
| `coach` | The agent alone. The customer hears nothing. |
|
|
4304
4325
|
| `join-call` | Everyone on the call. |
|
|
4305
4326
|
|
|
4306
|
-
All three come with the `lead` flag; a centre reserves none of them per lead.
|
|
4327
|
+
All three come with the `lead` flag; a centre reserves none of them per lead.
|
|
4307
4328
|
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4329
|
+
**There is no team audio.** Audio is the agent's or the lead's, and the lead's audio while
|
|
4330
|
+
listening follows the provider's published state exactly as a task's does: `listening` on the
|
|
4331
|
+
member is the provider's word that the lead's channel is in the call. When it appears, the
|
|
4332
|
+
application opens `openAudio({ assignmentId })` on the lead's connection for the call it names,
|
|
4333
|
+
and plays the `CallAudio` returned; when it goes, the application closes it. Nothing else says
|
|
4334
|
+
when the lead's audio is up, and a reload reopens it from the snapshot's team member list as a task
|
|
4335
|
+
carried with `audio: "started"` is reopened.
|
|
4336
|
+
|
|
4337
|
+
The flow, end to end:
|
|
4313
4338
|
|
|
4314
|
-
|
|
4339
|
+
```ts
|
|
4340
|
+
// 1. The lead, with the feature on, sees a member on-task with their tasks, and picks a call.
|
|
4341
|
+
// 2. The lead's channel joins it in silence. Conditions: the lead's voice channel is free --
|
|
4342
|
+
// not on a call, not listening elsewhere. Capacity is not involved: nothing is assigned.
|
|
4343
|
+
executeTeam({ command: { type: "listen", memberId: "A-1", assignmentId: "alloc-42" } })
|
|
4344
|
+
// 3. The provider republishes the team member list; the member carries
|
|
4345
|
+
// listening: { assignmentId: "alloc-42", mode: "listen", since }
|
|
4346
|
+
// -- the application opens openAudio({ assignmentId: "alloc-42" }) on the lead's connection.
|
|
4347
|
+
// 4. The lead changes how they are heard; the provider restates listening.mode. The audio stays open.
|
|
4315
4348
|
executeTeam({ command: { type: "coach", memberId: "A-1" } })
|
|
4316
4349
|
executeTeam({ command: { type: "join-call", memberId: "A-1" } })
|
|
4317
|
-
|
|
4318
|
-
// 3. The lead leaves, or takes the call.
|
|
4350
|
+
// 5. The lead leaves: the provider republishes the member without listening; the application closes the audio.
|
|
4319
4351
|
executeTeam({ command: { type: "leave", memberId: "A-1" } })
|
|
4352
|
+
// 6. The member's call ends: the same as leave, from the provider's side.
|
|
4353
|
+
// 7. The lead takes the call: the member loses listening no later than the offer, the application
|
|
4354
|
+
// closes the listening audio, and the lead's own call arrives as an ordinary assignment marked
|
|
4355
|
+
// takenOver, counted against capacity, with its audio following task-audio-started as any call's does.
|
|
4356
|
+
// At capacity the take-over is refused and the lead stays listening.
|
|
4320
4357
|
executeTeam({ command: { type: "take-over-call", memberId: "A-1" } })
|
|
4358
|
+
// 8. A join on the member's request is the same flow entered at step 3 with mode: "join-call",
|
|
4359
|
+
// and the member's task shows leadAssist: joined.
|
|
4360
|
+
// 9. A reload of the lead's application: the snapshot's team member list still carries listening,
|
|
4361
|
+
// so the audio is reopened.
|
|
4321
4362
|
```
|
|
4322
4363
|
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
to a login that declares `lead` (`event.teamAudio.*`). The member's own audio events are the
|
|
4328
|
-
member's and never reach the lead.
|
|
4329
|
-
|
|
4330
|
-
**`listening` on the member is the state**, restated on every change of mode, and a lead is on
|
|
4331
|
-
one member's call at a time (`team.listening.single`). The member's call ending ends the lead's
|
|
4332
|
-
listening with it: the provider republishes the member without `listening` and sends
|
|
4333
|
-
`team-audio-ended`.
|
|
4364
|
+
**`listening` on the member is the state**, restated on every change of mode, naming the call so
|
|
4365
|
+
the application knows which audio to open (`team.member.listening.assignmentId`, and one the
|
|
4366
|
+
member's tasks carry, `team.member.listening.assignment`), and a lead is on one member's call at a
|
|
4367
|
+
time (`team.listening.single`).
|
|
4334
4368
|
|
|
4335
4369
|
**A lead listens while holding no call of their own.** Omni offers Listen to a lead whose voice
|
|
4336
4370
|
channel is free, and a provider answers a `listen` from one whose channel is not `failed`. That
|
|
@@ -4689,7 +4723,7 @@ declared:
|
|
|
4689
4723
|
| --- | --- |
|
|
4690
4724
|
| `answer`, `accept` | Nothing. A task that was offered can be accepted, or offering it meant nothing. |
|
|
4691
4725
|
| `end-call` | The `endCall` capability: ends my part, the caller continues. |
|
|
4692
|
-
| `terminate-call` | The `terminateCall` capability: ends the
|
|
4726
|
+
| `terminate-call` | The `terminateCall` capability: ends the whole call, every channel on it. |
|
|
4693
4727
|
| `conference` with `action: "remove"` | The `conference` capability, and somebody else on the call: a remove that would leave the agent alone is `end-call`, and a provider answers it `failed`. |
|
|
4694
4728
|
| `decline` | The `decline` capability on any channel, **and** Omni local policy permitting it. One word for refusing an offer, whatever the channel. |
|
|
4695
4729
|
| `dial` | The `preview` phase. A record put in front of an agent is there to be called, so the phase is the gate and there is no capability. It is a dial, with a `dialId` and a `dial-outcome`. |
|