@xema/omni-protocol 0.1.37 → 0.1.39
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 +27 -27
- package/dist/index.js +3 -3
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +7 -7
- package/dist/validation.d.ts +2 -2
- package/dist/validation.js +16 -16
- package/guide.md +83 -76
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -173,8 +173,8 @@ export interface TeamCapabilities {
|
|
|
173
173
|
* `omni.capability-not-enabled`. Requires `executeTeamBreak`.
|
|
174
174
|
*/
|
|
175
175
|
breakControl?: true;
|
|
176
|
-
/** This lead may join a member's call on request. Requires `
|
|
177
|
-
|
|
176
|
+
/** This lead may join a member's call on request. Requires `executeTeamLeadAssist`. */
|
|
177
|
+
leadAssistControl?: true;
|
|
178
178
|
/** This lead sets the team's policy per capability -- on, off, or the agent's -- within what the queue allows. Requires `executeTeamPolicy`. */
|
|
179
179
|
policyControl?: true;
|
|
180
180
|
}
|
|
@@ -437,13 +437,13 @@ export type TaskCapabilities<C extends Channel = Channel> = C extends "voice" ?
|
|
|
437
437
|
mute?: Lockable<true>;
|
|
438
438
|
hold?: Lockable<true>;
|
|
439
439
|
agentDisconnect?: Lockable<true>;
|
|
440
|
-
/**
|
|
441
|
-
|
|
440
|
+
/** Connect back to the party while `completing`, whoever placed the call; the task returns to `in-progress`. */
|
|
441
|
+
connectBack?: Lockable<true>;
|
|
442
442
|
blindTransfer?: Lockable<true | DestinationDirectory>;
|
|
443
443
|
/** Park the customer and call a destination first; then `complete` or `cancel`. */
|
|
444
444
|
consultTransfer?: Lockable<true | DestinationDirectory>;
|
|
445
|
-
/** Ask a lead to join this call, with a note. The lead's decision arrives on `Task.
|
|
446
|
-
|
|
445
|
+
/** Ask a lead to join this call, with a note. The lead's decision arrives on `Task.leadAssist`. */
|
|
446
|
+
leadAssist?: Lockable<true>;
|
|
447
447
|
conference?: Lockable<true | DestinationDirectory>;
|
|
448
448
|
recording?: Lockable<true>;
|
|
449
449
|
} : C extends "chat" ? SharedTaskCapabilities & {
|
|
@@ -608,7 +608,7 @@ export type OnCall = {
|
|
|
608
608
|
* The agent's request for a lead, from asking until the lead leaves or the request ends.
|
|
609
609
|
* `requested` while nobody has joined; `joined`, with `leadId`, once somebody has.
|
|
610
610
|
*/
|
|
611
|
-
export interface
|
|
611
|
+
export interface TaskLeadAssist {
|
|
612
612
|
stage: "requested" | "joined";
|
|
613
613
|
leadId?: UserId;
|
|
614
614
|
note?: string;
|
|
@@ -616,7 +616,7 @@ export interface TaskLead {
|
|
|
616
616
|
}
|
|
617
617
|
/**
|
|
618
618
|
* On the lead's own task for a call they joined: which member asked, with their note. Its
|
|
619
|
-
* presence is what makes `lead` `take-over` and `leave` issuable.
|
|
619
|
+
* presence is what makes `lead-assist` `take-over` and `leave` issuable.
|
|
620
620
|
*/
|
|
621
621
|
export interface TaskAssisting {
|
|
622
622
|
memberId: UserId;
|
|
@@ -691,12 +691,12 @@ export type Task<C extends Channel = Channel> = {
|
|
|
691
691
|
handlingHistory?: TaskHandlingHistory;
|
|
692
692
|
} & TaskCompletion & (C extends "voice" ? {
|
|
693
693
|
onCall?: OnCall[];
|
|
694
|
-
|
|
694
|
+
leadAssist?: TaskLeadAssist;
|
|
695
695
|
assisting?: TaskAssisting;
|
|
696
696
|
media?: TaskMediaState;
|
|
697
697
|
} : {
|
|
698
698
|
onCall?: never;
|
|
699
|
-
|
|
699
|
+
leadAssist?: never;
|
|
700
700
|
assisting?: never;
|
|
701
701
|
media?: never;
|
|
702
702
|
});
|
|
@@ -730,7 +730,7 @@ export type TaskOutcome = {
|
|
|
730
730
|
failure: ProtocolFailure;
|
|
731
731
|
};
|
|
732
732
|
export declare const TASK_COMMAND_NAMES: {
|
|
733
|
-
readonly voice: readonly ["answer", "decline", "start-call", "mute", "hold", "resume", "disconnect", "
|
|
733
|
+
readonly voice: readonly ["answer", "decline", "start-call", "mute", "hold", "resume", "disconnect", "connect-back", "transfer", "lead-assist", "conference", "recording", "complete"];
|
|
734
734
|
readonly chat: readonly ["accept", "reject", "pause", "resume", "complete"];
|
|
735
735
|
readonly email: readonly ["accept", "reject", "complete"];
|
|
736
736
|
};
|
|
@@ -755,9 +755,9 @@ export type VoiceTaskCommand = {
|
|
|
755
755
|
} | {
|
|
756
756
|
type: "disconnect";
|
|
757
757
|
}
|
|
758
|
-
/** Issuable only in `completing`, under the `
|
|
758
|
+
/** Issuable only in `completing`, under the `connectBack` capability. Dials the party's own number, so it names none. */
|
|
759
759
|
| {
|
|
760
|
-
type: "
|
|
760
|
+
type: "connect-back";
|
|
761
761
|
dialId: DialId;
|
|
762
762
|
}
|
|
763
763
|
/** Blind: hand the customer to `destination` with nobody consulted. Gated by `blindTransfer`. */
|
|
@@ -784,25 +784,25 @@ export type VoiceTaskCommand = {
|
|
|
784
784
|
type: "transfer";
|
|
785
785
|
action: "cancel";
|
|
786
786
|
}
|
|
787
|
-
/** Ask a lead to join, with a note. Gated by `
|
|
787
|
+
/** Ask a lead to join, with a note. Gated by `leadAssist`. */
|
|
788
788
|
| {
|
|
789
|
-
type: "lead";
|
|
789
|
+
type: "lead-assist";
|
|
790
790
|
action: "request";
|
|
791
791
|
note?: string;
|
|
792
792
|
}
|
|
793
|
-
/** Withdraw a standing request. Needs `Task.
|
|
793
|
+
/** Withdraw a standing request. Needs `Task.leadAssist` with status `requested`. */
|
|
794
794
|
| {
|
|
795
|
-
type: "lead";
|
|
795
|
+
type: "lead-assist";
|
|
796
796
|
action: "cancel";
|
|
797
797
|
}
|
|
798
798
|
/** The lead keeps the customer; the agent's task ends `transferred`. Needs `Task.assisting`. */
|
|
799
799
|
| {
|
|
800
|
-
type: "lead";
|
|
800
|
+
type: "lead-assist";
|
|
801
801
|
action: "take-over";
|
|
802
802
|
}
|
|
803
803
|
/** The lead drops; the agent continues. The lead's task ends `left`. Needs `Task.assisting`. */
|
|
804
804
|
| {
|
|
805
|
-
type: "lead";
|
|
805
|
+
type: "lead-assist";
|
|
806
806
|
action: "leave";
|
|
807
807
|
}
|
|
808
808
|
/** Dial `destination` into the call. Gated by `conference`. */
|
|
@@ -999,7 +999,7 @@ export interface LeadRequest {
|
|
|
999
999
|
*/
|
|
1000
1000
|
export interface TeamRoster {
|
|
1001
1001
|
members: TeamMember[];
|
|
1002
|
-
/** Omitted when the login lacks `team.
|
|
1002
|
+
/** Omitted when the login lacks `team.leadAssistControl`; `[]` when nobody is asking. */
|
|
1003
1003
|
requests?: LeadRequest[];
|
|
1004
1004
|
/** The team's policy per capability, as it stands. Present exactly when the login declares `team.policyControl`. */
|
|
1005
1005
|
policies?: TeamPolicies;
|
|
@@ -1021,7 +1021,7 @@ export type TeamPolicyCommand = {
|
|
|
1021
1021
|
export interface TeamPolicyCommandRequest {
|
|
1022
1022
|
command: TeamPolicyCommand;
|
|
1023
1023
|
}
|
|
1024
|
-
export type
|
|
1024
|
+
export type TeamLeadAssistCommand = {
|
|
1025
1025
|
type: "join";
|
|
1026
1026
|
requestId: string;
|
|
1027
1027
|
} | {
|
|
@@ -1029,8 +1029,8 @@ export type TeamConsultCommand = {
|
|
|
1029
1029
|
requestId: string;
|
|
1030
1030
|
reason?: string;
|
|
1031
1031
|
};
|
|
1032
|
-
export interface
|
|
1033
|
-
command:
|
|
1032
|
+
export interface TeamLeadAssistCommandRequest {
|
|
1033
|
+
command: TeamLeadAssistCommand;
|
|
1034
1034
|
}
|
|
1035
1035
|
export type TeamBreakCommand = {
|
|
1036
1036
|
type: "decide";
|
|
@@ -1084,8 +1084,8 @@ export type OpenMediaResult = {
|
|
|
1084
1084
|
/**
|
|
1085
1085
|
* What the team may leave to the person: a capability by its own name -- `hold`, `mute` -- or a
|
|
1086
1086
|
* skill by its provider id. The same key as in `Task.capabilities`, because it is the same
|
|
1087
|
-
* capability seen at another level.
|
|
1088
|
-
* team's, on or off, within what the queue allows.
|
|
1087
|
+
* capability seen at another level. Connecting back and a new call are never the person's; they
|
|
1088
|
+
* are the team's, on or off, within what the queue allows.
|
|
1089
1089
|
*/
|
|
1090
1090
|
export type PreferenceId = "hold" | "mute" | `skill:${string}`;
|
|
1091
1091
|
/**
|
|
@@ -1294,8 +1294,8 @@ export interface Connection<C extends Channel = Channel> {
|
|
|
1294
1294
|
endBreak?(): Promise<BreakEndResult>;
|
|
1295
1295
|
/** Required when the login declares `capabilities.team.breakControl`. */
|
|
1296
1296
|
executeTeamBreak?(request: TeamBreakCommandRequest): Promise<TeamCommandResult>;
|
|
1297
|
-
/** Required when the login declares `capabilities.team.
|
|
1298
|
-
|
|
1297
|
+
/** Required when the login declares `capabilities.team.leadAssistControl`. */
|
|
1298
|
+
executeTeamLeadAssist?(request: TeamLeadAssistCommandRequest): Promise<TeamCommandResult>;
|
|
1299
1299
|
/** Required when the login declares `capabilities.team.policyControl`. */
|
|
1300
1300
|
executeTeamPolicy?(request: TeamPolicyCommandRequest): Promise<TeamCommandResult>;
|
|
1301
1301
|
/** Required of every voice adapter: all voice audio lands in Omni. */
|
package/dist/index.js
CHANGED
|
@@ -71,13 +71,13 @@ export function effectiveLevels(declared) {
|
|
|
71
71
|
// ---------------------------------------------------------------------------
|
|
72
72
|
export const TASK_COMMAND_NAMES = {
|
|
73
73
|
voice: ["answer", "decline", "start-call", "mute", "hold", "resume", "disconnect",
|
|
74
|
-
"
|
|
74
|
+
"connect-back", "transfer", "lead-assist", "conference", "recording", "complete"],
|
|
75
75
|
chat: ["accept", "reject", "pause", "resume", "complete"],
|
|
76
76
|
email: ["accept", "reject", "complete"],
|
|
77
77
|
};
|
|
78
78
|
/** The dial a command places, by the host's identity for it; `undefined` for a command that dials nothing. */
|
|
79
79
|
export function commandDialId(command) {
|
|
80
|
-
if (command.type === "
|
|
80
|
+
if (command.type === "connect-back")
|
|
81
81
|
return command.dialId;
|
|
82
82
|
if (command.type === "transfer" && (command.action === undefined || command.action === "consult"))
|
|
83
83
|
return command.dialId;
|
|
@@ -170,7 +170,7 @@ export function sameCapabilities(a, b) {
|
|
|
170
170
|
return a.breaks === b.breaks &&
|
|
171
171
|
(a.team === undefined) === (b.team === undefined) &&
|
|
172
172
|
a.team?.breakControl === b.team?.breakControl &&
|
|
173
|
-
a.team?.
|
|
173
|
+
a.team?.leadAssistControl === b.team?.leadAssistControl;
|
|
174
174
|
}
|
|
175
175
|
/**
|
|
176
176
|
* The storage-profile key a reusing browser shares, or `undefined` where it shares nothing.
|
package/dist/testing.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation }
|
|
|
7
7
|
* rules -- each optional part of a task, each optional part of the break state and roster, each
|
|
8
8
|
* declared contribution, and each event type.
|
|
9
9
|
*/
|
|
10
|
-
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.handlingHistory", "task.onCall", "task.
|
|
10
|
+
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.handlingHistory", "task.onCall", "task.leadAssist", "task.assisting", "task.media", "task.acceptance", "task.dispositions", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.imposed", "team.members", "team.requests", "contacts", "scheduledActivities", "team.policies"];
|
|
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
|
@@ -13,7 +13,7 @@ const STATE_SUBJECTS = [
|
|
|
13
13
|
"task.attributes",
|
|
14
14
|
"task.handlingHistory",
|
|
15
15
|
"task.onCall",
|
|
16
|
-
"task.
|
|
16
|
+
"task.leadAssist",
|
|
17
17
|
"task.assisting",
|
|
18
18
|
"task.media",
|
|
19
19
|
"task.acceptance",
|
|
@@ -56,8 +56,8 @@ function observeTask(value, seen) {
|
|
|
56
56
|
seen.add("task.handlingHistory");
|
|
57
57
|
if (some(value.onCall))
|
|
58
58
|
seen.add("task.onCall");
|
|
59
|
-
if (value.
|
|
60
|
-
seen.add("task.
|
|
59
|
+
if (value.leadAssist !== undefined)
|
|
60
|
+
seen.add("task.leadAssist");
|
|
61
61
|
if (value.assisting !== undefined)
|
|
62
62
|
seen.add("task.assisting");
|
|
63
63
|
if (value.media !== undefined)
|
|
@@ -223,8 +223,8 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
223
223
|
}
|
|
224
224
|
if (capabilities.team?.breakControl === true)
|
|
225
225
|
requireMethod(on, "executeTeamBreak", "the login declares capabilities.team.breakControl");
|
|
226
|
-
if (capabilities.team?.
|
|
227
|
-
requireMethod(on, "
|
|
226
|
+
if (capabilities.team?.leadAssistControl === true)
|
|
227
|
+
requireMethod(on, "executeTeamLeadAssist", "the login declares capabilities.team.leadAssistControl");
|
|
228
228
|
if (capabilities.team?.policyControl === true)
|
|
229
229
|
requireMethod(on, "executeTeamPolicy", "the login declares capabilities.team.policyControl");
|
|
230
230
|
if (some(capabilities.preferences))
|
|
@@ -404,7 +404,7 @@ function publishesUserIds(snapshot) {
|
|
|
404
404
|
}
|
|
405
405
|
const teamNamesUsers = (team) => isRecord(team) && (some(team.members) || some(team.requests));
|
|
406
406
|
const taskNamesUsers = (task) => isRecord(task) && ((isRecord(task.handlingHistory) && Array.isArray(task.handlingHistory.steps) && task.handlingHistory.steps.some(step => isRecord(step) && step.by !== undefined)) ||
|
|
407
|
-
(isRecord(task.
|
|
407
|
+
(isRecord(task.leadAssist) && task.leadAssist.leadId !== undefined) ||
|
|
408
408
|
isRecord(task.assisting));
|
|
409
409
|
/** Mute is the leg the host performs, so a task that allows it obliges the provider to take the record. */
|
|
410
410
|
const taskDeclaresMute = (task) => isRecord(task) && isRecord(task.capabilities) && task.capabilities.mute !== undefined;
|
|
@@ -517,7 +517,7 @@ export function assertCapabilityWithdrawal(states, snapshot, manifest) {
|
|
|
517
517
|
const withdrawn = (before.breaks === true && after.breaks !== true) ||
|
|
518
518
|
(before.team !== undefined && after.team === undefined) ||
|
|
519
519
|
(before.team?.breakControl === true && after.team?.breakControl !== true) ||
|
|
520
|
-
(before.team?.
|
|
520
|
+
(before.team?.leadAssistControl === true && after.team?.leadAssistControl !== true);
|
|
521
521
|
if (!withdrawn) {
|
|
522
522
|
throw new Error("Capability withdrawal must end with at least one capability the first login declared withdrawn");
|
|
523
523
|
}
|
package/dist/validation.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface ReaderContext {
|
|
|
33
33
|
self?: UserId;
|
|
34
34
|
/**
|
|
35
35
|
* The login's `AuthenticationState.capabilities`. The login is the permission: a lead's
|
|
36
|
-
* snapshot carries a roster, nobody else's does, and `requests` need `team.
|
|
36
|
+
* snapshot carries a roster, nobody else's does, and `requests` need `team.leadAssistControl`.
|
|
37
37
|
*/
|
|
38
38
|
capabilities?: UserCapabilities;
|
|
39
39
|
/** The level ids in force. Filled from the manifest by `validateSnapshot` and `validateEventEnvelope`; the defaults otherwise. */
|
|
@@ -59,7 +59,7 @@ export declare function validateHostGuarantees(guarantees: unknown, path?: strin
|
|
|
59
59
|
export declare function validateHandlingReport(report: unknown, path?: string, manifest?: unknown): ProtocolViolation[];
|
|
60
60
|
export declare function validateHostReport(report: unknown, path?: string): ProtocolViolation[];
|
|
61
61
|
/** The connection methods whose results `validateResult` knows. */
|
|
62
|
-
export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeamBreak" | "
|
|
62
|
+
export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeamBreak" | "executeTeamLeadAssist" | "openMedia" | "setPreference" | "recordStep" | "executeTeamPolicy";
|
|
63
63
|
/**
|
|
64
64
|
* Validates what a connection method answered. A result is untrusted for the same reason a
|
|
65
65
|
* snapshot is: it comes from an adapter that may be compiled against another version, and Omni
|
package/dist/validation.js
CHANGED
|
@@ -73,7 +73,7 @@ const EXPLAINED_FAILURES = ["busy", "no-answer", "unreachable", "rejected", "can
|
|
|
73
73
|
const ON_CALL_ROLES = membersOf({ party: true, agent: true, consulted: true, conferenced: true });
|
|
74
74
|
const ON_CALL_STAGES = membersOf({ ringing: true, joined: true });
|
|
75
75
|
/** The task capabilities under which a command dials, and so need the manifest to say how a dial ends. */
|
|
76
|
-
const DIALLING_CAPABILITIES = ["
|
|
76
|
+
const DIALLING_CAPABILITIES = ["connectBack", "blindTransfer", "consultTransfer", "conference"];
|
|
77
77
|
const SNAPSHOT_REASONS = membersOf({
|
|
78
78
|
reconnected: true, "provider-requested": true,
|
|
79
79
|
});
|
|
@@ -84,7 +84,7 @@ const MEMBER_BREAKS = membersOf({
|
|
|
84
84
|
const OFFERABLE_PHASES = membersOf({
|
|
85
85
|
pending: true, confirmed: true, preparing: true,
|
|
86
86
|
});
|
|
87
|
-
const TEAM_CAPABILITIES = membersOf({ breakControl: true,
|
|
87
|
+
const TEAM_CAPABILITIES = membersOf({ breakControl: true, leadAssistControl: true, policyControl: true });
|
|
88
88
|
const COMPLETED_BY = membersOf({ agent: true, provider: true });
|
|
89
89
|
const EXPIRABLE_PHASES = membersOf({
|
|
90
90
|
pending: true, confirmed: true, preparing: true,
|
|
@@ -94,7 +94,7 @@ const ISOLATION_SCHEME_VALUES = Object.values(BROWSER_ISOLATION_SCHEMES);
|
|
|
94
94
|
const TASK_CAPABILITIES = {
|
|
95
95
|
voice: membersOf({
|
|
96
96
|
browsers: true, dispositions: true, custom: true, decline: true, mute: true, hold: true,
|
|
97
|
-
agentDisconnect: true,
|
|
97
|
+
agentDisconnect: true, connectBack: true, blindTransfer: true, consultTransfer: true, leadAssist: true, conference: true, recording: true,
|
|
98
98
|
}),
|
|
99
99
|
chat: membersOf({ browsers: true, dispositions: true, custom: true, reject: true, hold: true }),
|
|
100
100
|
email: membersOf({ browsers: true, dispositions: true, custom: true, reject: true }),
|
|
@@ -799,26 +799,26 @@ function validateOnCall(value, channel, path, into) {
|
|
|
799
799
|
}
|
|
800
800
|
const LEAD_STAGES = membersOf({ requested: true, joined: true });
|
|
801
801
|
/** The agent's request for a lead. Voice only; `joined` names the lead, `requested` cannot. */
|
|
802
|
-
function
|
|
802
|
+
function validateLeadAssist(value, channel, path, into) {
|
|
803
803
|
if (value === undefined)
|
|
804
804
|
return;
|
|
805
|
-
if (!into.require(channel === "voice", "task.
|
|
805
|
+
if (!into.require(channel === "voice", "task.leadAssist.channel", path, `a ${channel} task cannot carry a lead request`))
|
|
806
806
|
return;
|
|
807
807
|
if (!isPlainObject(value)) {
|
|
808
|
-
into.add("task.
|
|
808
|
+
into.add("task.leadAssist.shape", path, "lead must be an object when present");
|
|
809
809
|
return;
|
|
810
810
|
}
|
|
811
|
-
if (into.oneOf(value.stage, LEAD_STAGES, "task.
|
|
811
|
+
if (into.oneOf(value.stage, LEAD_STAGES, "task.leadAssist.stage", `${path}.stage`)) {
|
|
812
812
|
if (value.stage === "joined") {
|
|
813
|
-
into.require(isUserId(value.leadId), "task.
|
|
813
|
+
into.require(isUserId(value.leadId), "task.leadAssist.leadId", `${path}.leadId`, "a joined lead is named by their user id");
|
|
814
814
|
}
|
|
815
815
|
else {
|
|
816
|
-
into.require(value.leadId === undefined, "task.
|
|
816
|
+
into.require(value.leadId === undefined, "task.leadAssist.leadId.unexpected", `${path}.leadId`, "nobody has joined a requested lead, so there is no lead to name");
|
|
817
817
|
}
|
|
818
818
|
}
|
|
819
819
|
if (value.note !== undefined)
|
|
820
|
-
into.filled(value.note, "task.
|
|
821
|
-
into.timestamp(value.since, "task.
|
|
820
|
+
into.filled(value.note, "task.leadAssist.note", `${path}.note`, "a note must not be empty when present");
|
|
821
|
+
into.timestamp(value.since, "task.leadAssist.since", `${path}.since`);
|
|
822
822
|
}
|
|
823
823
|
/** The lead's own task for a call they joined. Voice only. */
|
|
824
824
|
function validateAssisting(value, channel, path, into) {
|
|
@@ -886,7 +886,7 @@ function validateTaskInto(task, context, path, into) {
|
|
|
886
886
|
validateHandlingHistory(task.handlingHistory, `${path}.handlingHistory`, into);
|
|
887
887
|
validateOnCall(task.onCall, context.channel, `${path}.onCall`, into);
|
|
888
888
|
validateTaskMedia(task.media, context.channel, `${path}.media`, into);
|
|
889
|
-
|
|
889
|
+
validateLeadAssist(task.leadAssist, context.channel, `${path}.leadAssist`, into);
|
|
890
890
|
validateAssisting(task.assisting, context.channel, `${path}.assisting`, into);
|
|
891
891
|
const capabilities = task.capabilities;
|
|
892
892
|
if (!isPlainObject(capabilities)) {
|
|
@@ -1055,15 +1055,15 @@ function validateTeamRosterInto(roster, path, context, into) {
|
|
|
1055
1055
|
if (roster.requests === undefined) {
|
|
1056
1056
|
// `[]` says nobody is asking; omission says the lead may not be asked. A login that may be
|
|
1057
1057
|
// asked therefore always carries the list.
|
|
1058
|
-
if (context.capabilities?.team?.
|
|
1059
|
-
into.add("team.requests.required", `${path}.requests`, "the login declares team.
|
|
1058
|
+
if (context.capabilities?.team?.leadAssistControl === true) {
|
|
1059
|
+
into.add("team.requests.required", `${path}.requests`, "the login declares team.leadAssistControl, so the roster carries requests: [] when nobody is asking");
|
|
1060
1060
|
}
|
|
1061
1061
|
}
|
|
1062
1062
|
else {
|
|
1063
1063
|
// Requests are what a lead acts on, so a lead who may not act has no business receiving them.
|
|
1064
1064
|
// Whether they may is on the login, so the check needs the login in hand.
|
|
1065
1065
|
if (context.capabilities !== undefined) {
|
|
1066
|
-
into.require(context.capabilities.team?.
|
|
1066
|
+
into.require(context.capabilities.team?.leadAssistControl === true, "team.requests.capability", `${path}.requests`, "requests require team.leadAssistControl on the login: a lead who may not join has nothing to decide");
|
|
1067
1067
|
}
|
|
1068
1068
|
if (!Array.isArray(roster.requests)) {
|
|
1069
1069
|
into.add("team.requests.shape", `${path}.requests`, "requests must be an array when present");
|
|
@@ -1621,7 +1621,7 @@ const RESULT_STATUSES = {
|
|
|
1621
1621
|
cancelBreak: { success: "cancelled", failure: "failed" },
|
|
1622
1622
|
endBreak: { success: "ended", failure: "failed" },
|
|
1623
1623
|
executeTeamBreak: { success: "applied", failure: "failed" },
|
|
1624
|
-
|
|
1624
|
+
executeTeamLeadAssist: { success: "applied", failure: "failed" },
|
|
1625
1625
|
openMedia: { success: "opened", failure: "unavailable" },
|
|
1626
1626
|
setPreference: { success: "applied", failure: "failed" },
|
|
1627
1627
|
recordStep: { success: "recorded", failure: "failed" },
|
package/guide.md
CHANGED
|
@@ -35,7 +35,7 @@ are used precisely throughout and mean nothing looser here.
|
|
|
35
35
|
| **Event** | One completed transaction reported after a snapshot established the baseline. |
|
|
36
36
|
| **Break** | A reported, supervised state in which the agent is not working — one with a reason, a decision behind it and a return. It covers what a platform may call *not-ready*, including equipment trouble. An agent who is merely at capacity is not on a break. |
|
|
37
37
|
| **Workspace** | What Omni shows the agent. The **task workspace** holds the selected task, its controls and its browsers; the **idle workspace** holds what a provider contributes when no task is selected — dialpad, contacts, calendar, roster. |
|
|
38
|
-
| **Dial** | One outbound call the host asks a provider to place — from the idle dialpad, a transfer, a conference add, or a
|
|
38
|
+
| **Dial** | One outbound call the host asks a provider to place — from the idle dialpad, a transfer, a conference add, or a connect-back. Identified by the host's `dialId`, accepted as `dialling`, and ended by exactly one `dial-outcome`. See **Every dial has an outcome**. |
|
|
39
39
|
| **On the call** | Who a voice task's audio joins, or is bringing in, as the provider states it on `Task.onCall`: the party, the agents, and anyone consulted or conferenced in from the moment their dial is placed. |
|
|
40
40
|
|
|
41
41
|
Six words describe *what state a thing is in*, and they are not interchangeable: each belongs to
|
|
@@ -231,7 +231,7 @@ type AuthenticationContext = {
|
|
|
231
231
|
|
|
232
232
|
type TeamCapabilities = {
|
|
233
233
|
breakControl?: true;
|
|
234
|
-
|
|
234
|
+
leadAssistControl?: true;
|
|
235
235
|
policyControl?: true;
|
|
236
236
|
};
|
|
237
237
|
|
|
@@ -476,10 +476,10 @@ type TaskCapabilities<C extends Channel = Channel> =
|
|
|
476
476
|
mute?: Lockable<true>;
|
|
477
477
|
hold?: Lockable<true>;
|
|
478
478
|
agentDisconnect?: Lockable<true>;
|
|
479
|
-
|
|
479
|
+
connectBack?: Lockable<true>;
|
|
480
480
|
blindTransfer?: Lockable<true | DestinationDirectory>;
|
|
481
481
|
consultTransfer?: Lockable<true | DestinationDirectory>;
|
|
482
|
-
|
|
482
|
+
leadAssist?: Lockable<true>;
|
|
483
483
|
conference?: Lockable<true | DestinationDirectory>;
|
|
484
484
|
recording?: Lockable<true>;
|
|
485
485
|
}
|
|
@@ -597,7 +597,7 @@ type OnCall = { since: IsoTimestamp; held?: true } & (
|
|
|
597
597
|
| { role: "consulted" | "conferenced"; destination: string; stage: OnCallStage; dialId?: DialId; label?: string }
|
|
598
598
|
);
|
|
599
599
|
|
|
600
|
-
type
|
|
600
|
+
type TaskLeadAssist = {
|
|
601
601
|
stage: "requested" | "joined";
|
|
602
602
|
leadId?: UserId;
|
|
603
603
|
note?: string;
|
|
@@ -641,8 +641,8 @@ type Task<C extends Channel = Channel> = {
|
|
|
641
641
|
handlingHistory?: TaskHandlingHistory;
|
|
642
642
|
} & TaskCompletion & (
|
|
643
643
|
C extends "voice"
|
|
644
|
-
? { onCall?: OnCall[];
|
|
645
|
-
: { onCall?: never;
|
|
644
|
+
? { onCall?: OnCall[]; leadAssist?: TaskLeadAssist; assisting?: TaskAssisting; media?: TaskMediaState }
|
|
645
|
+
: { onCall?: never; leadAssist?: never; assisting?: never; media?: never }
|
|
646
646
|
);
|
|
647
647
|
|
|
648
648
|
type AcceptanceMode =
|
|
@@ -671,9 +671,9 @@ const TASK_COMMAND_NAMES = {
|
|
|
671
671
|
"hold",
|
|
672
672
|
"resume",
|
|
673
673
|
"disconnect",
|
|
674
|
-
"
|
|
674
|
+
"connect-back",
|
|
675
675
|
"transfer",
|
|
676
|
-
"lead",
|
|
676
|
+
"lead-assist",
|
|
677
677
|
"conference",
|
|
678
678
|
"recording",
|
|
679
679
|
"complete",
|
|
@@ -695,15 +695,15 @@ type VoiceTaskCommand =
|
|
|
695
695
|
| { type: "hold" }
|
|
696
696
|
| { type: "resume" }
|
|
697
697
|
| { type: "disconnect" }
|
|
698
|
-
| { type: "
|
|
698
|
+
| { type: "connect-back"; dialId: DialId }
|
|
699
699
|
| { type: "transfer"; dialId: DialId; destination: string; action?: never }
|
|
700
700
|
| { type: "transfer"; action: "consult"; dialId: DialId; destination: string }
|
|
701
701
|
| { type: "transfer"; action: "complete" }
|
|
702
702
|
| { type: "transfer"; action: "cancel" }
|
|
703
|
-
| { type: "lead"; action: "request"; note?: string }
|
|
704
|
-
| { type: "lead"; action: "cancel" }
|
|
705
|
-
| { type: "lead"; action: "take-over" }
|
|
706
|
-
| { type: "lead"; action: "leave" }
|
|
703
|
+
| { type: "lead-assist"; action: "request"; note?: string }
|
|
704
|
+
| { type: "lead-assist"; action: "cancel" }
|
|
705
|
+
| { type: "lead-assist"; action: "take-over" }
|
|
706
|
+
| { type: "lead-assist"; action: "leave" }
|
|
707
707
|
| { type: "conference"; action: "add"; dialId: DialId; destination: string }
|
|
708
708
|
| { type: "conference"; action: "remove"; destination: string }
|
|
709
709
|
| { type: "recording"; action: "start" | "pause" | "resume" | "stop" }
|
|
@@ -741,7 +741,7 @@ type TaskCommandResult =
|
|
|
741
741
|
### Who decides what an agent may do
|
|
742
742
|
|
|
743
743
|
An agent desk has two managers, not one. **The queue** — a process, a work type — is owned by a
|
|
744
|
-
process manager and **allows** a set of capabilities: hold, mute,
|
|
744
|
+
process manager and **allows** a set of capabilities: hold, mute, connect back, new call, conference,
|
|
745
745
|
whether the number is visible, the actions it offers, the skills it needs. **The people** are
|
|
746
746
|
managed through the organisation's structure — a team, a location, the organisation itself, in
|
|
747
747
|
whatever combination the structure defines for a person — and **decide** per capability within
|
|
@@ -788,7 +788,7 @@ controls — is content, and is never locked.
|
|
|
788
788
|
**A lead sets the team's policy from their roster.** A login that declares
|
|
789
789
|
`capabilities.team.policyControl` may `executeTeamPolicy({ type: "set", capability, setting })`
|
|
790
790
|
with `on`, `off`, or `person`, for any task control, `dial`, or a skill — and only `hold`, `mute`
|
|
791
|
-
and skills may be `person`;
|
|
791
|
+
and skills may be `person`; connect back and new call are the team's, on or off, within what the queue
|
|
792
792
|
allows. The roster carries `policies` for such a login: every policy as it stands, who set it, and
|
|
793
793
|
`lockedBy` where a level above the team made it theirs to keep, which the lead sees and cannot
|
|
794
794
|
change — `executeTeamPolicy` on it answers `failed` with `omni.capability-not-enabled`.
|
|
@@ -910,7 +910,7 @@ type TeamPolicyCommandRequest = {
|
|
|
910
910
|
command: TeamPolicyCommand;
|
|
911
911
|
};
|
|
912
912
|
|
|
913
|
-
type
|
|
913
|
+
type TeamLeadAssistCommand =
|
|
914
914
|
| { type: "join"; requestId: string }
|
|
915
915
|
| { type: "decline"; requestId: string; reason?: string };
|
|
916
916
|
|
|
@@ -924,8 +924,8 @@ type TeamBreakCommandRequest = {
|
|
|
924
924
|
command: TeamBreakCommand;
|
|
925
925
|
};
|
|
926
926
|
|
|
927
|
-
type
|
|
928
|
-
command:
|
|
927
|
+
type TeamLeadAssistCommandRequest = {
|
|
928
|
+
command: TeamLeadAssistCommand;
|
|
929
929
|
};
|
|
930
930
|
|
|
931
931
|
type TeamCommandResult =
|
|
@@ -1021,7 +1021,7 @@ type Connection<C extends Channel = Channel> = {
|
|
|
1021
1021
|
endBreak?(): Promise<BreakEndResult>;
|
|
1022
1022
|
|
|
1023
1023
|
executeTeamBreak?(request: TeamBreakCommandRequest): Promise<TeamCommandResult>;
|
|
1024
|
-
|
|
1024
|
+
executeTeamLeadAssist?(request: TeamLeadAssistCommandRequest): Promise<TeamCommandResult>;
|
|
1025
1025
|
openMedia?(request: OpenMediaRequest): Promise<OpenMediaResult>;
|
|
1026
1026
|
setPreference?(request: SetPreferenceRequest): Promise<PreferenceResult>;
|
|
1027
1027
|
recordStep?(report: HandlingReport): Promise<HandlingReportResult>;
|
|
@@ -1731,7 +1731,7 @@ them from what arrives later.
|
|
|
1731
1731
|
| `breaks` | This login may request a break. Requires the four break methods on the connection. |
|
|
1732
1732
|
| `team` | This login leads a team. The provider publishes a `TeamRoster` to it on every snapshot — `[]` when nobody is in it — and to nobody else. |
|
|
1733
1733
|
| `team.breakControl` | This lead may act on their team's breaks through `executeTeamBreak` — place, release, decide, set policy — as far as the provider supports; a command it lacks answers `omni.capability-not-enabled`. Omni asks for a decision only against a member whose `break` is `awaiting-decision`, so a provider that grants on request is never asked to decide. Requires `executeTeamBreak`. |
|
|
1734
|
-
| `team.
|
|
1734
|
+
| `team.leadAssistControl` | This lead may join a member's call on request. Requires `executeTeamLeadAssist`. |
|
|
1735
1735
|
| `team.policyControl` | This lead sets the team's policy per capability — on, off, or the person's — within what the queue allows. Requires `executeTeamPolicy`; the roster carries `policies`. |
|
|
1736
1736
|
| `preferences` | What the team left to this person, with where each stands and who set it. Omitted when nothing was. Requires `setPreference`. See **Who decides what an agent may do**. |
|
|
1737
1737
|
|
|
@@ -1927,7 +1927,7 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
1927
1927
|
| `cancelBreak()` | The login declares `capabilities.breaks`. |
|
|
1928
1928
|
| `endBreak()` | The login declares `capabilities.breaks`. |
|
|
1929
1929
|
| `executeTeamBreak(command)` | The login declares `capabilities.team.breakControl`. |
|
|
1930
|
-
| `
|
|
1930
|
+
| `executeTeamLeadAssist(command)` | The login declares `capabilities.team.leadAssistControl`. |
|
|
1931
1931
|
| `setPreference(request)` | The login declares `capabilities.preferences`: the person's choice has to have somewhere to go. |
|
|
1932
1932
|
| `recordStep(report)` | A task declares `mute`: the host performs that leg, and the provider's record has to have somewhere to take it. See **The host records what it performs**. |
|
|
1933
1933
|
| `executeTeamPolicy(command)` | The login declares `capabilities.team.policyControl`. |
|
|
@@ -2136,8 +2136,8 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2136
2136
|
| `attributes` | Optional ordered, typed `TaskAttribute` entries with keys unique within the task. Each contact or timestamp is a separate array item; new attribute shapes require new union members. |
|
|
2137
2137
|
| `handlingHistory` | The call record: `steps` — the ordered handling history of this open task, one entry per occurrence, oldest first — and what they add up to before this agent, `handleSeconds`, `holdSeconds`, `queueSeconds`, `transfers`, each present when the provider knows it. Live task data restated with the task, not a permanent archive. See **How a task has been handled**. |
|
|
2138
2138
|
| `onCall` | Voice only. Who is on the call, or being brought onto it, as the provider states it, replaced whole with the task: `party` is the customer, `agent` a person by user id, `consulted` and `conferenced` somebody a dial is bringing in, listed from the moment the dial is placed -- with the `destination` dialled, the `dialId` where a host placed it, the `stage` reached (`ringing` until answered, `joined` after), and `held: true` on anyone joined and parked. A `consulted` entry is what makes `transfer` `complete` and `cancel` issuable. `label` names a destination -- a person, a queue -- not a phrase; the host supplies the verb. Present when the provider knows the room, absent when it does not. See **Every dial has an outcome**. |
|
|
2139
|
-
| `
|
|
2140
|
-
| `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` `take-over` and `leave` issuable. See **
|
|
2139
|
+
| `leadAssist` | Voice only. Present from the agent's request for a lead until the lead leaves or the request ends: `requested` while nobody has joined, `joined` with the lead's `leadId` once somebody has. See **Lead assist**. |
|
|
2140
|
+
| `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**. |
|
|
2141
2141
|
|
|
2142
2142
|
`TaskAttribute` entries carry typed detail alongside the task:
|
|
2143
2143
|
|
|
@@ -2177,7 +2177,7 @@ The canonical task transitions are:
|
|
|
2177
2177
|
| `paused` | Provider or agent resumes the task | `in-progress` |
|
|
2178
2178
|
| `paused` | Agent cancels a consultation (`transfer` `cancel`) | `in-progress` |
|
|
2179
2179
|
| `in-progress` or `paused` | Contact handling ends and follow-up work remains | `completing` |
|
|
2180
|
-
| `completing` | Agent
|
|
2180
|
+
| `completing` | Agent connects back to the party (`connect-back`) | `in-progress` |
|
|
2181
2181
|
| Any phase | Provider emits `task-ended` | Removed |
|
|
2182
2182
|
|
|
2183
2183
|
Allocation, acceptance, and progress are distinct. Acceptance follows `autoAcceptTasks` and the
|
|
@@ -2188,7 +2188,7 @@ command.
|
|
|
2188
2188
|
**A task is never its audio.** A voice task is the allocation: the call is offered when it is
|
|
2189
2189
|
routed to the agent and accepted as its `acceptance` dictates, and its presence and phase follow
|
|
2190
2190
|
the provider's reports about the work — never the audio. Wherever audio moves — an offer, a hold, a
|
|
2191
|
-
consult, a conference leg joining or leaving, a transfer, a
|
|
2191
|
+
consult, a conference leg joining or leaving, a transfer, a connect-back — the media follows
|
|
2192
2192
|
separately, arriving on `task-media-started`, attaching through `openMedia` and ending with
|
|
2193
2193
|
`task-media-ended`. Omni does not ring,
|
|
2194
2194
|
bridge, or hold a line. How the phone rings, whether it rings at all, and where legs join and leave
|
|
@@ -2197,7 +2197,7 @@ phase it is in.
|
|
|
2197
2197
|
|
|
2198
2198
|
The line runs between the provider's word and Omni's own senses. `task-media-ended` is the
|
|
2199
2199
|
provider's report that primary handling ended — a fact about the work, which is why the completion
|
|
2200
|
-
allowance starts on it and the
|
|
2200
|
+
allowance starts on it and the Connect back control appears on it — and Omni follows that report as it
|
|
2201
2201
|
follows any other. What Omni never does is derive a task's state from its own media session: a
|
|
2202
2202
|
stream that drops, a track that ends, a transport that disconnects, a microphone that fails, an
|
|
2203
2203
|
endpoint re-registering change nothing about the task until the provider says so. Structurally:
|
|
@@ -2253,39 +2253,44 @@ and the agent takes as long as the work needs. Moving the task to `completing` l
|
|
|
2253
2253
|
deadline is not an alternative on a media channel: the clock starts at a real event, and delaying
|
|
2254
2254
|
that event would falsify the phase and everything timed from it.
|
|
2255
2255
|
|
|
2256
|
-
####
|
|
2256
|
+
#### Connecting back during completion
|
|
2257
2257
|
|
|
2258
|
-
A task that declares `
|
|
2259
|
-
`completing` -- to finish what the call left unfinished, on the same task rather than a new one
|
|
2260
|
-
|
|
2261
|
-
|
|
2258
|
+
A task that declares `connectBack` lets the agent connect back to the party while the task is
|
|
2259
|
+
`completing` -- to finish what the call left unfinished, on the same task rather than a new one,
|
|
2260
|
+
whoever placed the call in the first place. Most such calls came in, so this is not a redial: the
|
|
2261
|
+
agent rings a party who rang them. Omni issues `{ type: "connect-back", dialId }`; it is issuable
|
|
2262
|
+
only in `completing`, and only where the capability is declared. The provider knows who the party
|
|
2263
|
+
is; the command carries no destination, and it is a dial like any other, so it carries the host's
|
|
2264
|
+
`dialId`, is answered `dialling`, and ends in one `dial-outcome`.
|
|
2262
2265
|
|
|
2263
|
-
On `
|
|
2266
|
+
On `dialling` the provider is placing the call and the task returns to `in-progress`: the agent is
|
|
2264
2267
|
working again, and the wrap allowance is **discarded, not paused**. From there the call is
|
|
2265
2268
|
reported as any call is -- `paused`, `in-progress`, and when its media ends, `task-media-ended`
|
|
2266
|
-
again, which starts a fresh allowance from that instant. A party who does not answer is a
|
|
2267
|
-
whose media ended: the task returns to `completing` through the
|
|
2268
|
-
again from there. At no point is an agent dialling against a
|
|
2269
|
+
again, which starts a fresh allowance from that instant. A party who does not answer is a dial
|
|
2270
|
+
whose outcome says so and a call whose media ended: the task returns to `completing` through the
|
|
2271
|
+
same event and the clock starts again from there. At no point is an agent dialling against a
|
|
2272
|
+
deadline.
|
|
2269
2273
|
|
|
2270
2274
|
**The control exists only while there is a window to use it in.** Under `agent-command` the task
|
|
2271
2275
|
stays `completing` until the agent completes it, so the window is open for as long as they need.
|
|
2272
2276
|
Under `provider-automatic` the window is the allowance -- and with `wrapAllowance: 0` there
|
|
2273
|
-
is none: the provider disposes the task at provider end, and Omni does not offer
|
|
2274
|
-
the task declares. A capability names a control that can be used; on a task with no `completing`
|
|
2277
|
+
is none: the provider disposes the task at provider end, and Omni does not offer Connect back,
|
|
2278
|
+
whatever the task declares. A capability names a control that can be used; on a task with no `completing`
|
|
2275
2279
|
window it cannot, and declaring it there changes nothing.
|
|
2276
2280
|
|
|
2277
2281
|
```ts
|
|
2278
|
-
const
|
|
2282
|
+
const connectBackCapable = {
|
|
2279
2283
|
channel: "voice",
|
|
2280
|
-
capabilities: { hold: true,
|
|
2284
|
+
capabilities: { hold: true, connectBack: true, dispositions: true },
|
|
2281
2285
|
phase: "completing",
|
|
2282
2286
|
completionMode: "provider-automatic",
|
|
2283
2287
|
wrapAllowance: 30,
|
|
2284
2288
|
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "completionMode" | "wrapAllowance">;
|
|
2285
2289
|
```
|
|
2286
2290
|
|
|
2287
|
-
With ten seconds of the thirty left, the agent presses
|
|
2288
|
-
"
|
|
2291
|
+
With ten seconds of the thirty left, the agent presses Connect back: `execute({ command: { type:
|
|
2292
|
+
"connect-back", dialId } })` returns `dialling`, the task is `in-progress`, and the thirty seconds
|
|
2293
|
+
are gone.
|
|
2289
2294
|
The second call ends: `task-media-ended`, the task is `completing`, and a new thirty seconds runs
|
|
2290
2295
|
from that instant.
|
|
2291
2296
|
|
|
@@ -2560,10 +2565,10 @@ const taskCapabilities = {
|
|
|
2560
2565
|
| `mute` | Primary toggle: Mute | Omni may mute and unmute the agent's outbound audio. |
|
|
2561
2566
|
| `hold` | Primary toggle: Hold | Omni may issue voice-task `hold` and `resume` commands. |
|
|
2562
2567
|
| `agentDisconnect` | Primary button: Disconnect | Omni may disconnect real-time media without disposing the task. |
|
|
2563
|
-
| `
|
|
2568
|
+
| `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 disposes at provider end. See **Connecting back during completion**. |
|
|
2564
2569
|
| `blindTransfer` | Secondary menu item: Blind transfer | Omni may transfer the caller directly to a destination. |
|
|
2565
2570
|
| `consultTransfer` | Secondary menu item: Consult transfer | Omni may park the customer and call a destination first, then hand the customer over or cancel back. See **Consult transfer**. |
|
|
2566
|
-
| `
|
|
2571
|
+
| `leadAssist` | Secondary menu item: Lead assist | Omni may ask a lead to join this call, with a note. The lead's decision reaches the agent on `Task.leadAssist`. See **Lead assist**. |
|
|
2567
2572
|
| `conference` | Secondary button: Conference | Omni may dial a destination into the active call, and drop a `conferenced` entry from it -- one still ringing included, which calls the dial off. |
|
|
2568
2573
|
| `recording` | Overflow menu item: Recording | Omni may expose start, pause, resume, and stop recording controls. |
|
|
2569
2574
|
| `dispositions` | Primary button: Complete | Omni may request task disposal with a provider disposition and notes. |
|
|
@@ -2746,7 +2751,7 @@ inside Omni.
|
|
|
2746
2751
|
## Every dial has an outcome
|
|
2747
2752
|
|
|
2748
2753
|
Four commands place a call: `dial` from the idle dialpad, a blind or consult `transfer`, a
|
|
2749
|
-
`conference` `add`, and `
|
|
2754
|
+
`conference` `add`, and `connect-back`. Each is accepted or refused at once, and each then ends later
|
|
2750
2755
|
and apart from its answer -- the destination picks up, is busy, or never does -- and a dial placed
|
|
2751
2756
|
late in a call routinely outlives the call. Nothing in between is reported: the wire says
|
|
2752
2757
|
`dialling`, then says how it ended, once.
|
|
@@ -2806,7 +2811,7 @@ call it belonged to and never against the next one.
|
|
|
2806
2811
|
The stream knows a dial from three places: `TaskStream.dialled(dialId)`, which a host calls when it
|
|
2807
2812
|
places one; an entry on a task's `onCall`; and a step in its `handlingHistory`. Since a dialled entry
|
|
2808
2813
|
is listed from placement, `dialled()` is load-bearing only for a dial that never has an entry -- a
|
|
2809
|
-
blind transfer, a
|
|
2814
|
+
blind transfer, a connect-back, an idle dialpad call -- or whose entry has already left the room; an
|
|
2810
2815
|
outcome for a dial nobody placed and no task mentions is what `stream.dialOutcome.unknown` refuses.
|
|
2811
2816
|
|
|
2812
2817
|
**`answered` says what happened to the dial; `onCall` says who is in the room.** They are two
|
|
@@ -2824,7 +2829,7 @@ provider does report -- `busy`, `no-answer`, `unreachable`, `rejected` or `cance
|
|
|
2824
2829
|
(`manifest.dialOutcomes.unexplained.alone`): a provider that reports causes may honestly say the
|
|
2825
2830
|
switch gave none, while one that reports none would be sending the generic failure this set exists
|
|
2826
2831
|
to refuse. A provider that dials at all declares it: an idle
|
|
2827
|
-
dialpad requires it (`manifest.dialOutcomes.required`), and a task that declares `
|
|
2832
|
+
dialpad requires it (`manifest.dialOutcomes.required`), and a task that declares `connectBack`,
|
|
2828
2833
|
`blindTransfer`, `consultTransfer` or `conference` under a manifest without it is refused
|
|
2829
2834
|
(`task.capability.dialOutcomes.required`). Off voice nothing dials, and the field is a compile
|
|
2830
2835
|
error there.
|
|
@@ -2861,7 +2866,9 @@ answered, `joined` after. The `dial-outcome` is the transition and the stage is
|
|
|
2861
2866
|
pairing as `task-media-started` and `media`: an `answered` outcome and the task restated with the
|
|
2862
2867
|
entry `joined` say one thing twice, and any other outcome removes the entry. The outcome comes
|
|
2863
2868
|
first: a `task-updated` that itself moves an entry from `ringing` to `joined` before an `answered`
|
|
2864
|
-
outcome for its dial is refused (`stream.taskUpdated.stage`), as an update that moves `media` is.
|
|
2869
|
+
outcome for its dial is refused (`stream.taskUpdated.stage`), as an update that moves `media` is.
|
|
2870
|
+
The rule keys on the entry's `dialId`, so an entry the platform added itself, with none, is one
|
|
2871
|
+
whose move the stream cannot hold to an outcome: a clean stream proves nothing about ordering there. It is stated rather
|
|
2865
2872
|
than left to whoever placed the dial because that knowledge lives in one process: after a reload,
|
|
2866
2873
|
on a second session, or on a supervisor's screen the snapshot is the only source, and a room that
|
|
2867
2874
|
cannot tell ringing from joined says it contains someone who may never arrive. Nobody ringing is
|
|
@@ -3250,7 +3257,7 @@ A lead who also takes calls sees their team on the idle dashboard. `Snapshot.tea
|
|
|
3250
3257
|
| Field | Contract |
|
|
3251
3258
|
| --- | --- |
|
|
3252
3259
|
| `members` | Every member of this lead's team, whatever their state. `[]` says the lead has a team with nobody in it; omitting the roster says something else entirely — see **The login is the permission** below. |
|
|
3253
|
-
| `requests` | The members currently asking this lead to join a call, each with the task and the note. Required when the login declares `team.
|
|
3260
|
+
| `requests` | The members currently asking this lead to join a call, each with the task and the note. Required when the login declares `team.leadAssistControl`, `[]` when nobody is asking; omitted when it does not. See **Lead assist**. |
|
|
3254
3261
|
| `policies` | The team's policy per capability as it stands — the setting, who set it, and `lockedBy` where a level above the team made it theirs to keep. Required when the login declares `team.policyControl`; omitted when it does not. See **Who decides what an agent may do**. |
|
|
3255
3262
|
|
|
3256
3263
|
| `TeamMember` field | Contract |
|
|
@@ -3284,12 +3291,12 @@ that value does.
|
|
|
3284
3291
|
every snapshot, and to nobody else. Omni never decides who leads a team: the provider said so at
|
|
3285
3292
|
sign-in, and the roster agrees with it — present, `[]` included, for a lead; absent for everybody
|
|
3286
3293
|
else, which is the correct rendering for an agent who leads nobody. What the lead may do with the
|
|
3287
|
-
roster is on the login too, `team.breakControl` and `team.
|
|
3294
|
+
roster is on the login too, `team.breakControl` and `team.leadAssistControl`, never on the roster.
|
|
3288
3295
|
|
|
3289
3296
|
**The roster never carries the agent it is published to — not in `members`, and not in
|
|
3290
3297
|
`requests`.** A lead does not report to themself: their own break request and their own ask for a
|
|
3291
3298
|
lead go up to whoever leads them and appear on *that* person's roster, while the requester sees
|
|
3292
|
-
only their own `BreakState` and their task's `
|
|
3299
|
+
only their own `BreakState` and their task's `leadAssist` move. An adapter whose platform lists the lead
|
|
3293
3300
|
among their own members filters the signed-in identity out before publishing. **Being a lead is a
|
|
3294
3301
|
role the provider knows, never inferred from who is listed:** it is declared at sign-in, a lead
|
|
3295
3302
|
with nobody in their team publishes `[]`, an agent with no such role publishes nothing, and no
|
|
@@ -3319,37 +3326,37 @@ on their own `BreakState`, or they are stopped from working with no way to see w
|
|
|
3319
3326
|
What happens when no lead is online — auto-approving, for instance — is the provider's decision and is
|
|
3320
3327
|
never expressed here.
|
|
3321
3328
|
|
|
3322
|
-
###
|
|
3329
|
+
### Lead assist
|
|
3323
3330
|
|
|
3324
3331
|
An agent on a call may ask a lead to join it -- a dispute that needs approval, a customer who
|
|
3325
|
-
asks for a manager, a moment the agent wants a second pair of ears.
|
|
3326
|
-
`
|
|
3332
|
+
asks for a manager, a moment the agent wants a second pair of ears. A call centre calls this
|
|
3333
|
+
assistance or escalation, and the name says who assists: the capability is `leadAssist` on the task; the lead's side is the roster, which is already the lead's view of the
|
|
3327
3334
|
team, and a second lead method beside `executeTeamBreak`:
|
|
3328
3335
|
|
|
3329
3336
|
```ts
|
|
3330
|
-
|
|
3337
|
+
executeTeamLeadAssist({ command: TeamLeadAssistCommand }): Promise<TeamCommandResult>
|
|
3331
3338
|
```
|
|
3332
3339
|
|
|
3333
|
-
Required when the login declares `capabilities.team.
|
|
3340
|
+
Required when the login declares `capabilities.team.leadAssistControl`, and gated by it exactly as
|
|
3334
3341
|
`executeTeamBreak` is by `team.breakControl`. The flow, in order:
|
|
3335
3342
|
|
|
3336
3343
|
```ts
|
|
3337
|
-
// 1. The agent asks, with a small note. Their task carries `
|
|
3338
|
-
execute({ taskId: "call-42", command: { type: "lead", action: "request", note: "Refund dispute, needs approval" } })
|
|
3339
|
-
// task.
|
|
3344
|
+
// 1. The agent asks, with a small note. Their task carries `leadAssist` from here on.
|
|
3345
|
+
execute({ taskId: "call-42", command: { type: "lead-assist", action: "request", note: "Refund dispute, needs approval" } })
|
|
3346
|
+
// task.leadAssist = { status: "requested", note: "Refund dispute, needs approval", since }
|
|
3340
3347
|
|
|
3341
3348
|
// 2. Every lead entitled to it sees the request on their roster.
|
|
3342
3349
|
// team-updated: requests: [{ id: "req-7", memberId: "A-1", taskId: "call-42", note, since }]
|
|
3343
3350
|
|
|
3344
3351
|
// 3. A lead joins, or declines.
|
|
3345
|
-
|
|
3346
|
-
|
|
3352
|
+
executeTeamLeadAssist({ command: { type: "join", requestId: "req-7" } })
|
|
3353
|
+
executeTeamLeadAssist({ command: { type: "decline", requestId: "req-7", reason: "In a call" } })
|
|
3347
3354
|
```
|
|
3348
3355
|
|
|
3349
3356
|
**On `join` the provider bridges three parties and the lead is on a task of their own**, on the
|
|
3350
3357
|
same task id, arriving on the lead's connection as `task-offered` with `automatic`
|
|
3351
3358
|
-- the way a call an agent placed themselves arrives -- and carrying `assisting`. The agent's task
|
|
3352
|
-
moves to `
|
|
3359
|
+
moves to `leadAssist: { stage: "joined", leadId }`. A join is the lead's own act, so capacity does not
|
|
3353
3360
|
trigger it; but from then on it is an outstanding task the provider counts against the lead's
|
|
3354
3361
|
stated ceiling like any other, nothing more is allocated to the lead while it stands, and a
|
|
3355
3362
|
provider whose lead is already at the ceiling answers the join `failed`.
|
|
@@ -3364,8 +3371,8 @@ and a join is work. Omni offers Join to a lead only while their own `BreakState.
|
|
|
3364
3371
|
finish is not given more -- and a provider answers a `join` from a lead on such a break `failed`.
|
|
3365
3372
|
The request stands for another lead, as it does when this one is already on a call.
|
|
3366
3373
|
|
|
3367
|
-
**On `decline`, or a request the agent withdraws with `{ type: "lead", action: "cancel" }`, the
|
|
3368
|
-
provider clears `
|
|
3374
|
+
**On `decline`, or a request the agent withdraws with `{ type: "lead-assist", action: "cancel" }`, the
|
|
3375
|
+
provider clears `leadAssist` from the agent's task** and drops the request from every roster. Nothing
|
|
3369
3376
|
else changes; the agent is still on the call.
|
|
3370
3377
|
|
|
3371
3378
|
The lead then has two commands on their copy, gated by `assisting` being present, and a third
|
|
@@ -3373,8 +3380,8 @@ choice that is no command at all:
|
|
|
3373
3380
|
|
|
3374
3381
|
| The lead | The agent's task | The lead's task |
|
|
3375
3382
|
| --- | --- | --- |
|
|
3376
|
-
| `{ type: "lead", action: "take-over" }` | `task-ended` with `{ type: "transferred", destination: leadId }`, straight from `in-progress`: **no `completing` window**, the agent is idle at once | Continues alone, and ends as any call does |
|
|
3377
|
-
| `{ type: "lead", action: "leave" }` | Continues; `
|
|
3383
|
+
| `{ type: "lead-assist", action: "take-over" }` | `task-ended` with `{ type: "transferred", destination: leadId }`, straight from `in-progress`: **no `completing` window**, the agent is idle at once | Continues alone, and ends as any call does |
|
|
3384
|
+
| `{ type: "lead-assist", action: "leave" }` | Continues; `leadAssist` is cleared | `task-ended` with `{ type: "left" }` -- the call goes on without them |
|
|
3378
3385
|
| Stays until the customer hangs up | `task-media-ended`, `completing`, its own disposition | The same, independently: **both have the disposal window** |
|
|
3379
3386
|
|
|
3380
3387
|
`left` is the one outcome that ends a task without ending the call: this agent left a call that
|
|
@@ -3382,12 +3389,12 @@ continues without them. It reads as neither a completion nor a cancellation, bec
|
|
|
3382
3389
|
neither.
|
|
3383
3390
|
|
|
3384
3391
|
```ts
|
|
3385
|
-
const
|
|
3392
|
+
const leadAssistCapable = {
|
|
3386
3393
|
channel: "voice",
|
|
3387
|
-
capabilities: { hold: true,
|
|
3394
|
+
capabilities: { hold: true, leadAssist: true, dispositions: true },
|
|
3388
3395
|
phase: "in-progress",
|
|
3389
|
-
|
|
3390
|
-
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "
|
|
3396
|
+
leadAssist: { stage: "joined", leadId: "L-9", note: "Refund dispute, needs approval", since: "2026-08-21T09:04:00Z" },
|
|
3397
|
+
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "leadAssist">;
|
|
3391
3398
|
```
|
|
3392
3399
|
|
|
3393
3400
|
Lead and member alike are `UserId`s of this provider, so an adapter publishing them implements
|
|
@@ -3609,11 +3616,11 @@ declared:
|
|
|
3609
3616
|
| `decline`, `reject` | The channel's decline or reject capability, **and** Omni provisioning permitting rejection. |
|
|
3610
3617
|
| `start-call` | The `preparing` phase. It starts the contact a preview gave the agent time to read, so the phase is the gate and there is no capability. |
|
|
3611
3618
|
| `complete` | `completionMode: "agent-command"`. The `dispositions` capability decides whether a code travels with the command, never whether the command exists — a task Omni cannot complete never ends. |
|
|
3612
|
-
| `
|
|
3619
|
+
| `connect-back` | The `connectBack` capability **and** the `completing` phase. It exists to reach the party again after the call, so it has no meaning while the call is up. |
|
|
3613
3620
|
| `transfer` with `action: "consult"` | The `consultTransfer` capability. Blind `transfer` is gated by `blindTransfer`; the two are declared and offered separately. |
|
|
3614
3621
|
| `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`. |
|
|
3615
|
-
| `lead` with `action: "request"` or `"cancel"` | The `
|
|
3616
|
-
| `lead` 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`. |
|
|
3622
|
+
| `lead-assist` with `action: "request"` or `"cancel"` | The `leadAssist` capability. `cancel` needs a request standing -- `Task.leadAssist` with status `requested`. |
|
|
3623
|
+
| `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`. |
|
|
3617
3624
|
| Everything else | Its own named capability. |
|
|
3618
3625
|
|
|
3619
3626
|
Declining or rejecting a pending offer ends it without accepting or completing it. The provider
|
|
@@ -3856,7 +3863,7 @@ Every outcome ends the task for this agent. On `task-ended`, Omni:
|
|
|
3856
3863
|
- selects another task or returns to the idle workspace.
|
|
3857
3864
|
|
|
3858
3865
|
A `left` outcome ends the task for this agent alone: the call continues without them, as it does
|
|
3859
|
-
when a lead who joined it leaves -- see **
|
|
3866
|
+
when a lead who joined it leaves -- see **Lead assist**.
|
|
3860
3867
|
|
|
3861
3868
|
A successful `complete` or `transfer` command does not clear the task. Omni waits for `task-ended`.
|
|
3862
3869
|
The `task-media-ended` event and the `completing` phase are likewise non-terminal. A replacement
|
|
@@ -3980,7 +3987,7 @@ Some rules need to know who is reading. `validateTeamRoster`, `validateSnapshot`
|
|
|
3980
3987
|
`AuthenticationState.identity.id` and their login's `capabilities`. Given `self`, a roster that
|
|
3981
3988
|
carries that agent reports `team.member.self` or `team.request.self`. Given `capabilities`, a lead's
|
|
3982
3989
|
snapshot without a roster reports `team.required`, a roster published to a login that does not lead
|
|
3983
|
-
reports `team.unentitled`, `requests` on a roster whose login lacks `team.
|
|
3990
|
+
reports `team.unentitled`, `requests` on a roster whose login lacks `team.leadAssistControl` reports
|
|
3984
3991
|
`team.requests.capability`, and a roster without them on a login that declares it reports
|
|
3985
3992
|
`team.requests.required`. Without them those rules are not checked, because they cannot be.
|
|
3986
3993
|
`exerciseAdapter` always passes both.
|
|
@@ -4022,7 +4029,7 @@ latest the session published during the run, which differs only when the adapter
|
|
|
4022
4029
|
A capability granted by a later login requires its methods just as one declared at sign-in does.
|
|
4023
4030
|
|
|
4024
4031
|
`result.notExercised` lists what the run never reached — one subject per family of rules: each
|
|
4025
|
-
optional part of a task (`task.browsers`, `task.handlingHistory`, `task.
|
|
4032
|
+
optional part of a task (`task.browsers`, `task.handlingHistory`, `task.leadAssist`, …), the break's
|
|
4026
4033
|
`reasons` and `imposed`, the roster's `members` and `requests`, each declared contribution, and
|
|
4027
4034
|
each event type (`event.task-ended`, …) — and so what a clean `violations` says nothing about.
|
|
4028
4035
|
Nothing there is a violation: an adapter with no team has nothing to exercise. But a fixture with
|