@xema/omni-protocol 0.1.121 → 0.1.123
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/GLOSSARY.md +1 -1
- package/dist/index.d.ts +55 -11
- package/dist/index.js +26 -2
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +8 -1
- package/dist/validation.d.ts +2 -0
- package/dist/validation.js +72 -29
- package/guide/agent.md +23 -4
- package/guide.md +104 -29
- package/package.json +1 -1
package/GLOSSARY.md
CHANGED
|
@@ -100,7 +100,7 @@ hands only through routing or a lead's take-over.
|
|
|
100
100
|
|
|
101
101
|
| Term | In plain words | What others call it |
|
|
102
102
|
| --- | --- | --- |
|
|
103
|
-
| **History** | The steps a call went through before and with this agent: queued, answered, held, handed over. | *Contact trace record* (Amazon Connect), *conversation details* or *segments* (Genesys Cloud), *call detail record*. |
|
|
103
|
+
| **History** | The steps a call went through before and with this agent: arrived, self-service, queued, answered, held, handed over. | *Contact trace record* (Amazon Connect), *conversation details* or *segments* (Genesys Cloud), *call detail record*. |
|
|
104
104
|
| **Recording** | A recording of one side of the call, the customer's or the agent's. | *Call recording*, *screen and voice recording*. |
|
|
105
105
|
| **Paused for sensitive details** | Recording stops while card or account details are given. | *Secure pause* (Genesys Cloud), *PCI pause*. |
|
|
106
106
|
|
package/dist/index.d.ts
CHANGED
|
@@ -177,11 +177,31 @@ export type IdleCapabilities<C extends Channel = Channel> = {
|
|
|
177
177
|
} : {
|
|
178
178
|
dial?: never;
|
|
179
179
|
});
|
|
180
|
-
|
|
180
|
+
/** A BCP 47 language tag: `en`, `hi`, `en-IN`. */
|
|
181
|
+
export type LanguageTag = string;
|
|
182
|
+
/**
|
|
183
|
+
* One display text per language the provider words it in, keyed by language tag. The agent
|
|
184
|
+
* application shows the one for the agent's language, found with `localizedText()`, and its own
|
|
185
|
+
* word when the provider gave none in that language.
|
|
186
|
+
*/
|
|
187
|
+
export type LocalizedText = Readonly<Record<LanguageTag, string>>;
|
|
188
|
+
export type TaskPhaseLabels = Readonly<Partial<Record<TaskPhase, LocalizedText>>>;
|
|
189
|
+
export type HistoryStepLabels = Readonly<Partial<Record<HistoryStep, LocalizedText>>>;
|
|
181
190
|
export interface TaskTypePresentation {
|
|
182
|
-
singular:
|
|
183
|
-
plural:
|
|
184
|
-
referenceLabel?:
|
|
191
|
+
singular: LocalizedText;
|
|
192
|
+
plural: LocalizedText;
|
|
193
|
+
referenceLabel?: LocalizedText;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* What the signed-in customer calls things, as its own admin set them in the provider: one
|
|
197
|
+
* customer's agents read "New call" where another's read "Entered". Each part is partial and
|
|
198
|
+
* renames only; the values underneath stay the protocol's, so counts by phase or step add up.
|
|
199
|
+
*/
|
|
200
|
+
export interface Labels {
|
|
201
|
+
phases?: TaskPhaseLabels;
|
|
202
|
+
historySteps?: HistoryStepLabels;
|
|
203
|
+
/** Keyed by `taskType`. An entry replaces the channel default outright rather than merging. */
|
|
204
|
+
taskTypes?: Readonly<Record<string, TaskTypePresentation>>;
|
|
185
205
|
}
|
|
186
206
|
export interface Manifest<C extends Channel = Channel> {
|
|
187
207
|
/** Stable, installation-wide unique, and unchanged between launches. */
|
|
@@ -202,9 +222,6 @@ export interface Manifest<C extends Channel = Channel> {
|
|
|
202
222
|
timeCheck?: true;
|
|
203
223
|
/** Provider uses its own timestamps for final records; host timestamps are advisory. Omission makes no trust promise. */
|
|
204
224
|
timestampAuthority?: "provider";
|
|
205
|
-
phaseLabels?: TaskPhaseLabels;
|
|
206
|
-
/** Keyed by `taskType`. An entry replaces the channel default outright rather than merging. */
|
|
207
|
-
taskTypePresentation?: Record<string, TaskTypePresentation>;
|
|
208
225
|
/** The organisation's whole ladder, stated outright, `person` included. Omitted for the typical four, `DEFAULT_LEVELS`. */
|
|
209
226
|
orgLevels?: LevelDeclaration[];
|
|
210
227
|
/**
|
|
@@ -855,7 +872,7 @@ export type TaskAttribute = TaskAttributeBase & ({
|
|
|
855
872
|
type: "timestamp";
|
|
856
873
|
at: IsoTimestamp;
|
|
857
874
|
});
|
|
858
|
-
export type HistoryStep = "queued" | "offered" | "answered" | "held" | "muted" | "transferred" | "conferenced" | "unanswered" | "callback-requested";
|
|
875
|
+
export type HistoryStep = "arrived" | "self-service" | "queued" | "offered" | "answered" | "held" | "muted" | "transferred" | "conferenced" | "unanswered" | "callback-requested";
|
|
859
876
|
/**
|
|
860
877
|
* The call record: the steps that brought the task here, one entry per occurrence and oldest
|
|
861
878
|
* first, and what they add up to before this agent -- stated by the provider from its own record,
|
|
@@ -890,7 +907,7 @@ export interface TaskHistoryStep {
|
|
|
890
907
|
*/
|
|
891
908
|
seconds?: DurationSeconds;
|
|
892
909
|
/**
|
|
893
|
-
* Who took part. Absent on `queued`, where nobody does; absent on `callback-requested`, where the
|
|
910
|
+
* Who took part. Absent on `arrived`, `self-service` and `queued`, where nobody does; absent on `callback-requested`, where the
|
|
894
911
|
* caller chose it in the queue, and present there when an agent arranged it; absent on any other
|
|
895
912
|
* step means the provider could not attribute it, which is a different claim and a legitimate one.
|
|
896
913
|
*/
|
|
@@ -1756,6 +1773,8 @@ export interface Snapshot<C extends Channel = Channel> {
|
|
|
1756
1773
|
/** The call waiting in the agent's own queue, while one is; replaced whole by `lined-up`. */
|
|
1757
1774
|
linedUp?: LinedUpCall;
|
|
1758
1775
|
team?: TeamMembers;
|
|
1776
|
+
/** The signed-in customer's own words, from its admin's settings; replaced whole by `labels-updated`. Omitted where it set none. */
|
|
1777
|
+
labels?: Labels;
|
|
1759
1778
|
}
|
|
1760
1779
|
/**
|
|
1761
1780
|
* How many tasks this provider may have assigned to the agent at once.
|
|
@@ -1886,6 +1905,11 @@ export type ProviderEvent<C extends Channel = Channel> = {
|
|
|
1886
1905
|
| {
|
|
1887
1906
|
type: "lined-up";
|
|
1888
1907
|
linedUp?: LinedUpCall;
|
|
1908
|
+
}
|
|
1909
|
+
/** The customer's words as its admin now has them, whole: present with what it set, empty when it set nothing. */
|
|
1910
|
+
| {
|
|
1911
|
+
type: "labels-updated";
|
|
1912
|
+
labels: Labels;
|
|
1889
1913
|
};
|
|
1890
1914
|
/**
|
|
1891
1915
|
* The `Provider` prefix survives here for a mechanical reason rather than a naming one: `Event`
|
|
@@ -2027,6 +2051,25 @@ export declare const DEFAULT_TASK_TYPE_PRESENTATION: {
|
|
|
2027
2051
|
readonly referenceLabel: "Email ID";
|
|
2028
2052
|
};
|
|
2029
2053
|
};
|
|
2054
|
+
export declare const DEFAULT_HISTORY_STEP_LABELS: {
|
|
2055
|
+
readonly arrived: "Incoming call";
|
|
2056
|
+
readonly "self-service": "Self-service";
|
|
2057
|
+
readonly queued: "Queued";
|
|
2058
|
+
readonly offered: "Offered";
|
|
2059
|
+
readonly answered: "Answered";
|
|
2060
|
+
readonly held: "On hold";
|
|
2061
|
+
readonly muted: "Muted";
|
|
2062
|
+
readonly transferred: "Transferred";
|
|
2063
|
+
readonly conferenced: "Conferenced";
|
|
2064
|
+
readonly unanswered: "Unanswered";
|
|
2065
|
+
readonly "callback-requested": "Callback requested";
|
|
2066
|
+
};
|
|
2067
|
+
/**
|
|
2068
|
+
* The provider's text for the first of `languages` it has, matched as BCP 47 lookup does: the tag
|
|
2069
|
+
* whole, then with its last subtag dropped, so `hi-IN` finds `hi`. Undefined when it has none of
|
|
2070
|
+
* them, and the agent application shows its own word; never another language in their place.
|
|
2071
|
+
*/
|
|
2072
|
+
export declare function localizedText(text: LocalizedText | undefined, languages: readonly LanguageTag[]): string | undefined;
|
|
2030
2073
|
/**
|
|
2031
2074
|
* A collision-safe global task key.
|
|
2032
2075
|
*
|
|
@@ -2044,8 +2087,9 @@ export declare const assignmentKey: (providerId: string, assignmentId: Assignmen
|
|
|
2044
2087
|
*/
|
|
2045
2088
|
export declare const userKey: (providerId: string, userId: UserId) => string;
|
|
2046
2089
|
/**
|
|
2047
|
-
* Every history step somebody from the provider takes part in. `
|
|
2048
|
-
* `callback-requested` is the caller's choice unless an agent arranged it, so
|
|
2090
|
+
* Every history step somebody from the provider takes part in. `arrived`, `self-service` and
|
|
2091
|
+
* `queued` are the ones nobody does, and `callback-requested` is the caller's choice unless an agent arranged it, so none
|
|
2092
|
+
* of them is here.
|
|
2049
2093
|
*/
|
|
2050
2094
|
export declare const HISTORY_STEPS_WITH_A_PERSON: readonly ["offered", "answered", "held", "muted", "transferred", "conferenced", "unanswered"];
|
|
2051
2095
|
/** The steps a dial writes, and so the only ones that carry a `dialId` and a `destinationId`. */
|
package/dist/index.js
CHANGED
|
@@ -145,6 +145,29 @@ export const DEFAULT_TASK_TYPE_PRESENTATION = {
|
|
|
145
145
|
chat: { singular: "Chat", plural: "Chats", referenceLabel: "Chat ID" },
|
|
146
146
|
email: { singular: "Email", plural: "Emails", referenceLabel: "Email ID" },
|
|
147
147
|
};
|
|
148
|
+
export const DEFAULT_HISTORY_STEP_LABELS = {
|
|
149
|
+
arrived: "Incoming call", "self-service": "Self-service", queued: "Queued", offered: "Offered", answered: "Answered", held: "On hold",
|
|
150
|
+
muted: "Muted", transferred: "Transferred", conferenced: "Conferenced", unanswered: "Unanswered",
|
|
151
|
+
"callback-requested": "Callback requested",
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* The provider's text for the first of `languages` it has, matched as BCP 47 lookup does: the tag
|
|
155
|
+
* whole, then with its last subtag dropped, so `hi-IN` finds `hi`. Undefined when it has none of
|
|
156
|
+
* them, and the agent application shows its own word; never another language in their place.
|
|
157
|
+
*/
|
|
158
|
+
export function localizedText(text, languages) {
|
|
159
|
+
if (text === undefined)
|
|
160
|
+
return undefined;
|
|
161
|
+
const byTag = new Map(Object.entries(text).map(([tag, value]) => [tag.toLowerCase(), value]));
|
|
162
|
+
for (const language of languages) {
|
|
163
|
+
for (let tag = language.toLowerCase(); tag !== ""; tag = tag.includes("-") ? tag.slice(0, tag.lastIndexOf("-")) : "") {
|
|
164
|
+
const found = byTag.get(tag);
|
|
165
|
+
if (found !== undefined)
|
|
166
|
+
return found;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
148
171
|
// ---------------------------------------------------------------------------
|
|
149
172
|
// Utilities.
|
|
150
173
|
// ---------------------------------------------------------------------------
|
|
@@ -165,8 +188,9 @@ export const assignmentKey = (providerId, assignmentId) => `${encodeURIComponent
|
|
|
165
188
|
*/
|
|
166
189
|
export const userKey = (providerId, userId) => `${encodeURIComponent(providerId)}:${encodeURIComponent(userId)}`;
|
|
167
190
|
/**
|
|
168
|
-
* Every history step somebody from the provider takes part in. `
|
|
169
|
-
* `callback-requested` is the caller's choice unless an agent arranged it, so
|
|
191
|
+
* Every history step somebody from the provider takes part in. `arrived`, `self-service` and
|
|
192
|
+
* `queued` are the ones nobody does, and `callback-requested` is the caller's choice unless an agent arranged it, so none
|
|
193
|
+
* of them is here.
|
|
170
194
|
*/
|
|
171
195
|
export const HISTORY_STEPS_WITH_A_PERSON = [
|
|
172
196
|
"offered", "answered", "held", "muted", "transferred", "conferenced", "unanswered",
|
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.request", "team.tasks", "team.listening", "team.shift", "team.phone", "team.nextCall", "team.linedUp", "shift", "phone", "nextCall", "linedUp", "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.request", "team.tasks", "team.listening", "team.shift", "team.phone", "team.nextCall", "team.linedUp", "shift", "phone", "nextCall", "linedUp", "labels", "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
|
@@ -35,6 +35,7 @@ const STATE_SUBJECTS = [
|
|
|
35
35
|
"phone",
|
|
36
36
|
"nextCall",
|
|
37
37
|
"linedUp",
|
|
38
|
+
"labels",
|
|
38
39
|
"contacts",
|
|
39
40
|
"calendar",
|
|
40
41
|
"team.policies",
|
|
@@ -46,6 +47,7 @@ const EVENT_TYPES = {
|
|
|
46
47
|
"task-audio-started": true, "task-audio-ended": true, "task-ended": true, "dial-outcome": true, announcement: true, "queue-summary": true, diagnostic: true,
|
|
47
48
|
"team-updated": true, "team-member-updated": true, "team-member-removed": true, "team-policies-updated": true,
|
|
48
49
|
"contacts-updated": true, "calendar-updated": true, "shift-updated": true, "phone-updated": true, "next-call": true, "lined-up": true,
|
|
50
|
+
"labels-updated": true,
|
|
49
51
|
};
|
|
50
52
|
const CONTRACT_SUBJECTS = [
|
|
51
53
|
...STATE_SUBJECTS,
|
|
@@ -147,6 +149,8 @@ function observeSnapshot(value, seen) {
|
|
|
147
149
|
seen.add("nextCall");
|
|
148
150
|
if (value.linedUp !== undefined)
|
|
149
151
|
seen.add("linedUp");
|
|
152
|
+
if (value.labels !== undefined)
|
|
153
|
+
seen.add("labels");
|
|
150
154
|
}
|
|
151
155
|
function observeEvent(envelope, seen) {
|
|
152
156
|
const event = isRecord(envelope) ? envelope.event : undefined;
|
|
@@ -197,6 +201,9 @@ function observeEvent(envelope, seen) {
|
|
|
197
201
|
if (event.linedUp !== undefined)
|
|
198
202
|
seen.add("linedUp");
|
|
199
203
|
break;
|
|
204
|
+
case "labels-updated":
|
|
205
|
+
seen.add("labels");
|
|
206
|
+
break;
|
|
200
207
|
default: break;
|
|
201
208
|
}
|
|
202
209
|
}
|
|
@@ -1154,7 +1161,7 @@ const REACHABLE_PHASES = {
|
|
|
1154
1161
|
export const SUPERSEDED_BY_A_SNAPSHOT = new Set([
|
|
1155
1162
|
"snapshot", "transport-status", "break-state", "task-offered", "task-updated", "task-ended",
|
|
1156
1163
|
"task-audio-started", "task-audio-ended", "team-updated", "team-member-updated", "team-member-removed", "team-policies-updated",
|
|
1157
|
-
"contacts-updated", "calendar-updated", "shift-updated", "phone-updated", "next-call", "lined-up",
|
|
1164
|
+
"contacts-updated", "calendar-updated", "shift-updated", "phone-updated", "next-call", "lined-up", "labels-updated",
|
|
1158
1165
|
]);
|
|
1159
1166
|
export class TaskStream {
|
|
1160
1167
|
tasks = new Map();
|
package/dist/validation.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export declare function validateProviderTimeCheckRequest(value: unknown, path?:
|
|
|
30
30
|
/** Checks response shape/correlation, not source-clock accuracy, round-trip delay or freshness. */
|
|
31
31
|
export declare function validateProviderTimeCheckResult(value: unknown, request: unknown, loginId: string, path?: string): ProtocolViolation[];
|
|
32
32
|
export declare function validateManifest(manifest: unknown, path?: string): ProtocolViolation[];
|
|
33
|
+
/** The customer's own words, as `Snapshot.labels` and `labels-updated` carry them. */
|
|
34
|
+
export declare function validateLabels(value: unknown, path?: string): ProtocolViolation[];
|
|
33
35
|
export interface TaskValidationContext {
|
|
34
36
|
/** The provider's channel, from its manifest. A task must agree with it. */
|
|
35
37
|
channel: string;
|
package/dist/validation.js
CHANGED
|
@@ -58,7 +58,7 @@ const TEAM_AVAILABILITIES = membersOf({
|
|
|
58
58
|
ready: true, "on-task": true, "on-break": true, elsewhere: true, "signed-out": true,
|
|
59
59
|
});
|
|
60
60
|
const HISTORY_STEPS = membersOf({
|
|
61
|
-
queued: true, offered: true, answered: true, held: true, muted: true, transferred: true, conferenced: true, unanswered: true,
|
|
61
|
+
arrived: true, "self-service": true, queued: true, offered: true, answered: true, held: true, muted: true, transferred: true, conferenced: true, unanswered: true,
|
|
62
62
|
"callback-requested": true,
|
|
63
63
|
});
|
|
64
64
|
const CUSTOM_UI_CONTROLS = membersOf({ button: true, toggle: true, "menu-item": true });
|
|
@@ -517,16 +517,9 @@ export function validateManifest(manifest, path = "manifest") {
|
|
|
517
517
|
into.require(manifest.timeCheck === true, "manifest.timeCheck", `${path}.timeCheck`, "declare true or omit unsupported clock checks");
|
|
518
518
|
validateDialOutcomes(manifest, `${path}.dialOutcomes`, into);
|
|
519
519
|
validatePhones(manifest, `${path}.phones`, into);
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
else {
|
|
525
|
-
for (const [phase, label] of Object.entries(manifest.phaseLabels)) {
|
|
526
|
-
into.oneOf(phase, TASK_PHASES, "manifest.phaseLabels.phase", `${path}.phaseLabels.${phase}`);
|
|
527
|
-
into.filled(label, "manifest.phaseLabels.label", `${path}.phaseLabels.${phase}`, "a phase label must not be empty");
|
|
528
|
-
}
|
|
529
|
-
}
|
|
520
|
+
// Labels are the signed-in customer's, set by its own admin: a manifest is fixed when the adapter ships.
|
|
521
|
+
for (const former of ["phaseLabels", "historyStepLabels", "taskTypePresentation"]) {
|
|
522
|
+
into.require(!Object.hasOwn(manifest, former), "manifest.labels.moved", `${path}.${former}`, "labels are the signed-in customer's own words and travel as Snapshot.labels and labels-updated; the manifest carries none");
|
|
530
523
|
}
|
|
531
524
|
if (manifest.runningStepReports !== undefined) {
|
|
532
525
|
into.require(manifest.runningStepReports === true, "manifest.runningStepReports", `${path}.runningStepReports`, "runningStepReports is declared by presence, as true; a provider that takes begin and end only omits it");
|
|
@@ -566,26 +559,67 @@ export function validateManifest(manifest, path = "manifest") {
|
|
|
566
559
|
}
|
|
567
560
|
}
|
|
568
561
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
562
|
+
return into.violations;
|
|
563
|
+
}
|
|
564
|
+
/** Language tags as BCP 47 writes them: a language, then subtags of up to eight letters or digits. */
|
|
565
|
+
const LANGUAGE_TAG = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{1,8})*$/;
|
|
566
|
+
/** The customer's own words, as `Snapshot.labels` and `labels-updated` carry them. */
|
|
567
|
+
export function validateLabels(value, path = "labels") {
|
|
568
|
+
const into = new Collector();
|
|
569
|
+
validateLabelsInto(value, path, into);
|
|
570
|
+
return into.violations;
|
|
571
|
+
}
|
|
572
|
+
function validateLabelsInto(value, path, into) {
|
|
573
|
+
if (!isPlainObject(value)) {
|
|
574
|
+
into.add("labels.shape", path, "labels are an object, empty when the customer set none");
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
for (const [field, rule, allowed] of [["phases", "phase", TASK_PHASES], ["historySteps", "step", HISTORY_STEPS]]) {
|
|
578
|
+
if (value[field] === undefined)
|
|
579
|
+
continue;
|
|
580
|
+
if (!isPlainObject(value[field])) {
|
|
581
|
+
into.add(`labels.${field}.shape`, `${path}.${field}`, `${field} must be an object when present`);
|
|
582
|
+
continue;
|
|
572
583
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
584
|
+
for (const [key, label] of Object.entries(value[field])) {
|
|
585
|
+
// Renames only: a label never adds a phase or a step the protocol does not have.
|
|
586
|
+
into.oneOf(key, allowed, `labels.${field}.${rule}`, `${path}.${field}.${key}`);
|
|
587
|
+
validateLocalizedText(label, `labels.${field}.label`, `${path}.${field}.${key}`, into);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (value.taskTypes !== undefined) {
|
|
591
|
+
if (!isPlainObject(value.taskTypes)) {
|
|
592
|
+
into.add("labels.taskTypes.shape", `${path}.taskTypes`, "taskTypes must be an object when present");
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
for (const [taskType, presentation] of Object.entries(value.taskTypes)) {
|
|
596
|
+
const at = `${path}.taskTypes.${taskType}`;
|
|
597
|
+
if (!isPlainObject(presentation)) {
|
|
598
|
+
into.add("labels.taskTypes.entry", at, "each task type's names must be an object");
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
validateLocalizedText(presentation.singular, "labels.taskTypes.singular", `${at}.singular`, into);
|
|
602
|
+
validateLocalizedText(presentation.plural, "labels.taskTypes.plural", `${at}.plural`, into);
|
|
603
|
+
if (presentation.referenceLabel !== undefined) {
|
|
604
|
+
validateLocalizedText(presentation.referenceLabel, "labels.taskTypes.referenceLabel", `${at}.referenceLabel`, into);
|
|
585
605
|
}
|
|
586
606
|
}
|
|
587
607
|
}
|
|
588
|
-
|
|
608
|
+
}
|
|
609
|
+
/** One text per language: at least one, each tag well-formed and stated once, each text filled. */
|
|
610
|
+
function validateLocalizedText(value, rule, path, into) {
|
|
611
|
+
if (!isPlainObject(value) || Object.keys(value).length === 0) {
|
|
612
|
+
into.add(rule, path, `a label is one text per language, keyed by language tag, as { en: "On Call" }; received ${describeValue(value)}`);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const seen = new Set();
|
|
616
|
+
for (const [tag, text] of Object.entries(value)) {
|
|
617
|
+
if (!into.require(LANGUAGE_TAG.test(tag), `${rule}.language`, `${path}.${tag}`, `${describeValue(tag)} is not a language tag such as en, hi or en-IN`))
|
|
618
|
+
continue;
|
|
619
|
+
into.require(!seen.has(tag.toLowerCase()), `${rule}.language.unique`, `${path}.${tag}`, "language tags are case-insensitive, and each language is stated once");
|
|
620
|
+
seen.add(tag.toLowerCase());
|
|
621
|
+
into.filled(text, `${rule}.text`, `${path}.${tag}`, "a label must not be empty in any language it is given in");
|
|
622
|
+
}
|
|
589
623
|
}
|
|
590
624
|
// ---------------------------------------------------------------------------
|
|
591
625
|
// Task.
|
|
@@ -921,6 +955,10 @@ function validateHistory(value, path, into, task = {}) {
|
|
|
921
955
|
return;
|
|
922
956
|
}
|
|
923
957
|
into.oneOf(entry.step, HISTORY_STEPS, "task.history.step", `${at}.step`);
|
|
958
|
+
// A call arrives once, before anything else happens to it.
|
|
959
|
+
if (entry.step === "arrived") {
|
|
960
|
+
into.require(index === 0, "task.history.arrived.first", `${at}.step`, "arrived is when the call reached the provider: the first step, and only once");
|
|
961
|
+
}
|
|
924
962
|
if (into.timestamp(entry.at, "task.history.at", `${at}.at`)) {
|
|
925
963
|
// The record is one entry per occurrence, oldest first: a second hold is a second entry
|
|
926
964
|
// after the first, never a revision of it or an entry filed out of its turn.
|
|
@@ -957,8 +995,8 @@ function validateHistory(value, path, into, task = {}) {
|
|
|
957
995
|
}
|
|
958
996
|
if (entry.by !== undefined) {
|
|
959
997
|
into.require(isUserId(entry.by), "task.history.by", `${at}.by`, "by must be a non-empty user id; omit it when the person cannot be identified");
|
|
960
|
-
// On `queued` nobody takes part, so there is nothing to name.
|
|
961
|
-
into.require(entry.step !== "queued", "task.history.by.unexpected", `${at}.by`,
|
|
998
|
+
// On `arrived`, `self-service` and `queued` nobody takes part, so there is nothing to name.
|
|
999
|
+
into.require(entry.step !== "arrived" && entry.step !== "self-service" && entry.step !== "queued", "task.history.by.unexpected", `${at}.by`, `${describeValue(entry.step)} names nobody`);
|
|
962
1000
|
}
|
|
963
1001
|
// A step that dialled says where to and, when a host placed it, which dial; no other step dialled.
|
|
964
1002
|
const dialled = typeof entry.step === "string" && HISTORY_STEPS_THAT_DIAL.includes(entry.step);
|
|
@@ -1789,6 +1827,8 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot", context
|
|
|
1789
1827
|
// The agent's own day, as the provider counts it: the same numbers their lead sees.
|
|
1790
1828
|
if (snapshot.shift !== undefined)
|
|
1791
1829
|
validateShiftInto(snapshot.shift, `${path}.shift`, into, "shift");
|
|
1830
|
+
if (snapshot.labels !== undefined)
|
|
1831
|
+
validateLabelsInto(snapshot.labels, `${path}.labels`, into);
|
|
1792
1832
|
// The phone as the platform sees it: owed by a provider that declares it can, and by nobody else.
|
|
1793
1833
|
const seesPhone = isPlainObject(manifest) && manifest.phoneStatus === true;
|
|
1794
1834
|
// The agent's own queue: the ask and the lined-up call need an assignment at work to be next
|
|
@@ -2138,6 +2178,9 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
|
|
|
2138
2178
|
case "shift-updated":
|
|
2139
2179
|
validateShiftInto(event.shift, `${at}.shift`, into, "shift");
|
|
2140
2180
|
break;
|
|
2181
|
+
case "labels-updated":
|
|
2182
|
+
validateLabelsInto(event.labels, `${at}.labels`, into);
|
|
2183
|
+
break;
|
|
2141
2184
|
case "calendar-updated":
|
|
2142
2185
|
into.require(idle.calendar === true, "event.calendar.capability", `${at}.calendar`, "calendar-updated requires the calendar idle capability");
|
|
2143
2186
|
if (!Array.isArray(event.calendar)) {
|
package/guide/agent.md
CHANGED
|
@@ -409,6 +409,8 @@ first, and what they add up to before this agent:
|
|
|
409
409
|
```ts
|
|
410
410
|
history: {
|
|
411
411
|
steps: [
|
|
412
|
+
{ step: "arrived", at: "2026-08-21T00:58:40Z", seconds: 2 },
|
|
413
|
+
{ step: "self-service", at: "2026-08-21T00:58:42Z", seconds: 18 },
|
|
412
414
|
{ step: "queued", at: "2026-08-21T00:59:00Z", seconds: 41 },
|
|
413
415
|
{ step: "answered", at: "2026-08-21T00:59:41Z", seconds: 312, by: "a-17" },
|
|
414
416
|
{ step: "held", at: "2026-08-21T01:02:10Z", seconds: 35, by: "a-17" },
|
|
@@ -429,9 +431,26 @@ something else, which will arrive under its own name and must not be folded in h
|
|
|
429
431
|
|
|
430
432
|
It rides in the snapshot and is replaced whole like everything else there.
|
|
431
433
|
|
|
432
|
-
Steps are `queued`, `offered`, `answered`, `held`, `muted`, `transferred`, `conferenced`,
|
|
434
|
+
Steps are `arrived`, `self-service`, `queued`, `offered`, `answered`, `held`, `muted`, `transferred`, `conferenced`,
|
|
433
435
|
`unanswered`, `callback-requested`, and each is defined on `TaskHistoryStep`.
|
|
434
436
|
|
|
437
|
+
**The record starts where the call started.** `arrived` is the instant the call reached the
|
|
438
|
+
provider, before any menu or queue, and its `seconds`, when the provider measured them, are how
|
|
439
|
+
long it took to move on to the next step. Nobody takes part in it, so it never has `by`. It is the
|
|
440
|
+
first step and appears once (`task.history.arrived.first`); a provider that did not see the call
|
|
441
|
+
arrive omits it rather than guessing an instant. Omni shows it as *Incoming call* unless the customer words it in
|
|
442
|
+
`labels.historySteps`, the way
|
|
443
|
+
Amazon Connect keeps an initiation time, Genesys Cloud a conversation start and NICE CXone a contact
|
|
444
|
+
start, separate from the queue.
|
|
445
|
+
|
|
446
|
+
**Time with the IVR or a bot is `self-service`.** The caller pressing through a menu, talking to a
|
|
447
|
+
voice bot, or a chat customer with a bot before a person takes over: one step for all of them, on
|
|
448
|
+
every channel, the way Genesys Cloud and NICE CXone report IVR and bot time together as
|
|
449
|
+
self-service. Nobody from the provider takes part, so it has no `by`. `at` is when the customer
|
|
450
|
+
entered it and `seconds`, once it ended, how long they spent there; a customer sent back to a menu
|
|
451
|
+
after a transfer has a second entry, one per visit, as a second queue is a second `queued`. A
|
|
452
|
+
customer that words it *IVR* for its agents does so in `labels.historySteps`.
|
|
453
|
+
|
|
435
454
|
**A callback carries the call it comes back to.** When a caller asks to be called back, or an agent
|
|
436
455
|
arranges it, the provider writes a `callback-requested` step: `by` names the agent who arranged it, and
|
|
437
456
|
is absent when the caller chose it in the queue. The callback reaches an agent as a new task, and that
|
|
@@ -465,7 +484,7 @@ hold, at the entry's `at` plus its `seconds`, and nothing is lost by not naming
|
|
|
465
484
|
`muted` entries, and a call that joined two queues -- a menu's, then this one -- has two `queued`
|
|
466
485
|
entries, one per join. Order is enforced: an entry earlier than the one before it is refused
|
|
467
486
|
(`task.history.order`). **Intervals between steps are the agent application's to subtract**, never a
|
|
468
|
-
total the provider adds: *time to offer*, from the call
|
|
487
|
+
total the provider adds: *time to offer*, from the call joining its queue to the agent's screen lighting
|
|
469
488
|
up, is the `offered` entry's `at` minus the last `queued` entry's, and the ring is outside it.
|
|
470
489
|
`queueSeconds` keeps the industry's meaning -- the whole wait until somebody answered -- and is not
|
|
471
490
|
that interval. **The second is the record's grain.** A duration is a positive whole number of
|
|
@@ -541,7 +560,7 @@ Four rules a provider has to keep:
|
|
|
541
560
|
wrongly; carrying the name here would also copy it into every task and leave it to go stale.
|
|
542
561
|
|
|
543
562
|
**An absent `by` means different things on different steps, and both are legitimate.** On
|
|
544
|
-
`queued` nobody takes part, so there is nothing to name. On every other step somebody did — see
|
|
563
|
+
`arrived`, `self-service` and `queued` nobody takes part, so there is nothing to name. On every other step somebody did — see
|
|
545
564
|
`HISTORY_STEPS_WITH_A_PERSON` and `historyStepExpectsAPerson()` — so an absent `by` there says
|
|
546
565
|
*this was handled and the provider cannot say by whom*. The one step that is never unattributed is
|
|
547
566
|
`muted`: the agent application has exactly one agent and the provider knows who, so `by` is required there
|
|
@@ -554,7 +573,7 @@ worse than one saying plainly it could not attribute a leg — and far better th
|
|
|
554
573
|
nothing because a single leg could not be named.
|
|
555
574
|
|
|
556
575
|
An agent application must render the two differently. Showing an unattributed `answered` the same way as
|
|
557
|
-
`queued` tells the agent nobody was involved, which is not what was said. Omni renders it as
|
|
576
|
+
`queued`, `self-service` or `arrived` tells the agent nobody was involved, which is not what was said. Omni renders it as
|
|
558
577
|
*"not recorded"* in the place the name would go.
|
|
559
578
|
|
|
560
579
|
Omit `history` entirely when the provider cannot observe the steps. Empty `steps` is a
|
package/guide.md
CHANGED
|
@@ -233,8 +233,6 @@ type Manifest<C extends Channel = Channel> = {
|
|
|
233
233
|
idleCapabilities?: IdleCapabilities<C>;
|
|
234
234
|
timeCheck?: true;
|
|
235
235
|
timestampAuthority?: "provider";
|
|
236
|
-
phaseLabels?: TaskPhaseLabels;
|
|
237
|
-
taskTypePresentation?: Record<string, TaskTypePresentation>;
|
|
238
236
|
orgLevels?: LevelDeclaration[];
|
|
239
237
|
dialOutcomes?: C extends "voice" ? DialOutcome[] : never;
|
|
240
238
|
phones?: C extends "voice" ? Phone[] : never;
|
|
@@ -259,12 +257,22 @@ These serialized strings are stable protocol values and must not be renamed or r
|
|
|
259
257
|
### Presentation
|
|
260
258
|
|
|
261
259
|
```ts
|
|
262
|
-
type
|
|
260
|
+
type LanguageTag = string; // BCP 47: "en", "hi", "en-IN"
|
|
261
|
+
type LocalizedText = Readonly<Record<LanguageTag, string>>;
|
|
262
|
+
|
|
263
|
+
type TaskPhaseLabels = Readonly<Partial<Record<TaskPhase, LocalizedText>>>;
|
|
264
|
+
type HistoryStepLabels = Readonly<Partial<Record<HistoryStep, LocalizedText>>>;
|
|
263
265
|
|
|
264
266
|
type TaskTypePresentation = {
|
|
265
|
-
singular:
|
|
266
|
-
plural:
|
|
267
|
-
referenceLabel?:
|
|
267
|
+
singular: LocalizedText;
|
|
268
|
+
plural: LocalizedText;
|
|
269
|
+
referenceLabel?: LocalizedText;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
type Labels = {
|
|
273
|
+
phases?: TaskPhaseLabels;
|
|
274
|
+
historySteps?: HistoryStepLabels;
|
|
275
|
+
taskTypes?: Readonly<Record<string, TaskTypePresentation>>;
|
|
268
276
|
};
|
|
269
277
|
```
|
|
270
278
|
|
|
@@ -524,6 +532,7 @@ type Snapshot = {
|
|
|
524
532
|
nextCall?: NextCallRequest;
|
|
525
533
|
linedUp?: LinedUpCall;
|
|
526
534
|
team?: TeamMembers;
|
|
535
|
+
labels?: Labels;
|
|
527
536
|
};
|
|
528
537
|
|
|
529
538
|
type AgentCapacity = {
|
|
@@ -686,6 +695,8 @@ type TaskAttribute = TaskAttributeBase & (
|
|
|
686
695
|
);
|
|
687
696
|
|
|
688
697
|
type HistoryStep =
|
|
698
|
+
| "arrived"
|
|
699
|
+
| "self-service"
|
|
689
700
|
| "queued"
|
|
690
701
|
| "offered"
|
|
691
702
|
| "answered"
|
|
@@ -1218,7 +1229,8 @@ type ProviderEvent =
|
|
|
1218
1229
|
| { type: "shift-updated"; shift: Shift }
|
|
1219
1230
|
| { type: "phone-updated"; phone: PhoneState }
|
|
1220
1231
|
| { type: "next-call"; nextCall?: NextCallRequest }
|
|
1221
|
-
| { type: "lined-up"; linedUp?: LinedUpCall }
|
|
1232
|
+
| { type: "lined-up"; linedUp?: LinedUpCall }
|
|
1233
|
+
| { type: "labels-updated"; labels: Labels };
|
|
1222
1234
|
|
|
1223
1235
|
type ProviderEventEnvelope = {
|
|
1224
1236
|
id: string;
|
|
@@ -1351,8 +1363,8 @@ const OMNI_FAILURE_CODES = [
|
|
|
1351
1363
|
type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
|
|
1352
1364
|
```
|
|
1353
1365
|
|
|
1354
|
-
`HISTORY_STEPS_WITH_A_PERSON` is every `HistoryStep` except `
|
|
1355
|
-
takes part in. `historyStepExpectsAPerson()` tests membership.
|
|
1366
|
+
`HISTORY_STEPS_WITH_A_PERSON` is every `HistoryStep` except `arrived`, `self-service` and `queued`, which nobody
|
|
1367
|
+
takes part in, and `callback-requested`, which the caller may choose alone. `historyStepExpectsAPerson()` tests membership.
|
|
1356
1368
|
|
|
1357
1369
|
### Failure and validation
|
|
1358
1370
|
|
|
@@ -1373,25 +1385,83 @@ interface ProtocolViolation {
|
|
|
1373
1385
|
|
|
1374
1386
|
## Presentation labels
|
|
1375
1387
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1388
|
+
**Each customer's admin chooses the words its agents see, in each language it serves**, in the
|
|
1389
|
+
provider, and the provider publishes them for the signed-in customer as `Snapshot.labels`,
|
|
1390
|
+
replaced whole by `labels-updated` when the admin changes them. Customer A's agents read *New call*
|
|
1391
|
+
where customer B's read *Entered*, on the same adapter, and a change reaches agents without an
|
|
1392
|
+
adapter release. That is how Genesys Cloud loads an organisation's presence names at sign-in and
|
|
1393
|
+
pushes the change when an admin edits one, and how Amazon Connect and NICE CXone keep agent state
|
|
1394
|
+
names per instance or business unit. A manifest is fixed when the adapter ships, so it carries no
|
|
1395
|
+
labels (`manifest.labels.moved`). `labels` is omitted, or `labels-updated` carries `{}`, when the
|
|
1396
|
+
customer set nothing, and the agent application shows its own words throughout.
|
|
1397
|
+
|
|
1398
|
+
```ts
|
|
1399
|
+
labels: {
|
|
1400
|
+
phases: { "in-progress": { en: "On Call", hi: "कॉल पर" } },
|
|
1401
|
+
historySteps: { arrived: { en: "New call", hi: "नई कॉल" }, queued: { en: "Waiting" } },
|
|
1402
|
+
taskTypes: { Support: { singular: { en: "Support call" }, plural: { en: "Support calls" }, referenceLabel: { en: "Case" } } },
|
|
1403
|
+
}
|
|
1404
|
+
```
|
|
1405
|
+
|
|
1406
|
+
`validateLabels(value, path?)` checks one on its own.
|
|
1407
|
+
|
|
1408
|
+
**Every word is per language.** Every label is a
|
|
1409
|
+
`LocalizedText`: one text per BCP 47 language tag, as `{ en: "On Call", hi: "कॉल पर" }`. The agent
|
|
1410
|
+
application finds the agent's language with `localizedText(text, languages)`, which tries each of
|
|
1411
|
+
the agent's languages in order, the tag whole and then without its last subtag, so `hi-IN` finds
|
|
1412
|
+
`hi`. When the provider wrote none of them, it returns nothing and the agent application shows its
|
|
1413
|
+
own word; it never shows another language in their place. A single string is the former shape and
|
|
1414
|
+
is refused (`labels.phases.label`, `labels.historySteps.label`,
|
|
1415
|
+
`labels.taskTypes.singular`), as are an empty text (`….text`), a key that is not a language tag
|
|
1416
|
+
(`….language`) and one language given twice in different case (`….language.unique`). This is how
|
|
1417
|
+
Genesys Cloud words its presence definitions and NICE CXone its agent states: the value is the
|
|
1418
|
+
platform's, and the words are per language.
|
|
1419
|
+
|
|
1420
|
+
The values underneath never change: a customer renames what its agents see, so a lead's counts by
|
|
1421
|
+
phase or by step still add up.
|
|
1378
1422
|
|
|
1379
1423
|
### Renaming a phase
|
|
1380
1424
|
|
|
1381
|
-
`
|
|
1382
|
-
phase or removes one.
|
|
1425
|
+
`labels.phases` renames the canonical `TaskPhase` values for the agent. It never adds a
|
|
1426
|
+
phase or removes one (`labels.phases.phase`).
|
|
1383
1427
|
|
|
1384
1428
|
`TaskPhaseLabels` is `Partial` so that renaming one phase keeps the default wording for the rest —
|
|
1385
|
-
|
|
1429
|
+
a customer changing "On Call" does not restate the other five.
|
|
1386
1430
|
|
|
1387
|
-
**A task is labelled by the provider that owns it**, from that provider's
|
|
1431
|
+
**A task is labelled by the provider that owns it**, from that provider's `labels` merged over its
|
|
1388
1432
|
channel defaults. Two adapters may word the same phase differently and each is right about its own
|
|
1389
1433
|
tasks: a call reads as the voice platform names it while a chat beside it reads as its own does.
|
|
1390
1434
|
How Omni words a view spanning several providers is its own presentation problem, not a provider's.
|
|
1391
1435
|
|
|
1436
|
+
### Naming the history steps
|
|
1437
|
+
|
|
1438
|
+
`labels.historySteps` words the `HistoryStep` values the way `labels.phases` words phases: per
|
|
1439
|
+
step, per language, and partial, so a customer that calls `arrived` *Entered* restates nothing
|
|
1440
|
+
else. It never adds a step; a provider's own stages that the protocol has no step for are not
|
|
1441
|
+
labelled into existence (`labels.historySteps.step`).
|
|
1442
|
+
|
|
1443
|
+
`DEFAULT_HISTORY_STEP_LABELS` is what Omni shows when the customer words a step in none of the
|
|
1444
|
+
agent's languages:
|
|
1445
|
+
|
|
1446
|
+
```ts
|
|
1447
|
+
const DEFAULT_HISTORY_STEP_LABELS = {
|
|
1448
|
+
arrived: "Incoming call",
|
|
1449
|
+
"self-service": "Self-service",
|
|
1450
|
+
queued: "Queued",
|
|
1451
|
+
offered: "Offered",
|
|
1452
|
+
answered: "Answered",
|
|
1453
|
+
held: "On hold",
|
|
1454
|
+
muted: "Muted",
|
|
1455
|
+
transferred: "Transferred",
|
|
1456
|
+
conferenced: "Conferenced",
|
|
1457
|
+
unanswered: "Unanswered",
|
|
1458
|
+
"callback-requested": "Callback requested",
|
|
1459
|
+
} as const satisfies Readonly<Record<HistoryStep, string>>;
|
|
1460
|
+
```
|
|
1461
|
+
|
|
1392
1462
|
### `DEFAULT_TASK_PHASE_LABELS`
|
|
1393
1463
|
|
|
1394
|
-
What Omni shows when
|
|
1464
|
+
What Omni shows when the customer renames nothing.
|
|
1395
1465
|
|
|
1396
1466
|
```ts
|
|
1397
1467
|
const DEFAULT_TASK_PHASE_LABELS = {
|
|
@@ -1426,14 +1496,14 @@ const DEFAULT_TASK_PHASE_LABELS = {
|
|
|
1426
1496
|
|
|
1427
1497
|
### Naming a kind of work
|
|
1428
1498
|
|
|
1429
|
-
`TaskTypePresentation` says what one kind of work is called
|
|
1430
|
-
**replaces the channel default outright** rather than merging.
|
|
1499
|
+
`TaskTypePresentation` says what one kind of work is called, in `labels.taskTypes`. Unlike
|
|
1500
|
+
`labels.phases`, an entry **replaces the channel default outright** rather than merging.
|
|
1431
1501
|
`singular` and `plural` are required together because pluralisation is not mechanical, and a
|
|
1432
1502
|
half-supplied entry would leave Omni pairing one provider's noun with another's plural.
|
|
1433
1503
|
|
|
1434
1504
|
### `DEFAULT_TASK_TYPE_PRESENTATION`
|
|
1435
1505
|
|
|
1436
|
-
The per-channel fallback, used for any `taskType` the
|
|
1506
|
+
The per-channel fallback, used for any `taskType` the customer does not name.
|
|
1437
1507
|
|
|
1438
1508
|
```ts
|
|
1439
1509
|
const DEFAULT_TASK_TYPE_PRESENTATION = {
|
|
@@ -1453,21 +1523,21 @@ const DEFAULT_TASK_TYPE_PRESENTATION = {
|
|
|
1453
1523
|
referenceLabel: "Email ID",
|
|
1454
1524
|
},
|
|
1455
1525
|
} as const satisfies Readonly<
|
|
1456
|
-
Record<"voice" | "chat" | "email",
|
|
1526
|
+
Record<"voice" | "chat" | "email", { singular: string; plural: string; referenceLabel: string }>
|
|
1457
1527
|
>;
|
|
1458
1528
|
```
|
|
1459
1529
|
|
|
1460
1530
|
### Naming task types
|
|
1461
1531
|
|
|
1462
1532
|
`channel` is too coarse for agent-facing names. A WhatsApp call and a PSTN call are both `voice`,
|
|
1463
|
-
but the
|
|
1533
|
+
but the customer may name them differently:
|
|
1464
1534
|
|
|
1465
1535
|
```ts
|
|
1466
|
-
const
|
|
1536
|
+
const taskTypes = {
|
|
1467
1537
|
WhatsApp: {
|
|
1468
|
-
singular: "Conversation",
|
|
1469
|
-
plural: "Conversations",
|
|
1470
|
-
referenceLabel: "Conversation ID",
|
|
1538
|
+
singular: { en: "Conversation", hi: "बातचीत" },
|
|
1539
|
+
plural: { en: "Conversations", hi: "बातचीतें" },
|
|
1540
|
+
referenceLabel: { en: "Conversation ID" },
|
|
1471
1541
|
},
|
|
1472
1542
|
} satisfies Record<string, TaskTypePresentation>;
|
|
1473
1543
|
```
|
|
@@ -1710,8 +1780,6 @@ compile time.
|
|
|
1710
1780
|
| `supportedProtocolVersions` | Required non-empty list of versions this adapter can speak. Must share at least one with Omni. |
|
|
1711
1781
|
| `authenticationMethods` | Required non-empty list of supported login methods. |
|
|
1712
1782
|
| `idleCapabilities` | Declares actions Omni may offer while the agent has no active task, such as voice dialing. Task controls do not belong here. |
|
|
1713
|
-
| `phaseLabels` | Optional static adapter-defined display names for canonical `TaskPhase` values. They cannot vary at runtime. |
|
|
1714
|
-
| `taskTypePresentation` | Optional static adapter-defined presentation keyed by exact `taskType`. It names the item and its optional agent-facing reference. |
|
|
1715
1783
|
| `orgLevels` | The organisation's whole ladder as the provider calls it, each level with the label an agent computer shows for "who decided". Stated outright, `person` included: what it leaves out does not exist. Omitted for the typical four, `DEFAULT_LEVELS`. See **Who decides what an agent may do**. |
|
|
1716
1784
|
| `phones` | Voice only, and required there: the phones this platform can put an agent on, `softphone` (the call's audio lands in the agent application) and/or `hardphone` (a handset the platform rings; the agent application shows the call and opens nothing). The agent application picks one per login. See **How the agent hears the call** in `guide/phone.md`. |
|
|
1717
1785
|
| `phoneStatus` | Voice only, declared by presence as `true`: the platform sees the phone itself -- its registration, its do-not-disturb, its hook, its calls -- and publishes it as `Snapshot.phone` and `phone-updated`. A platform that cannot see the phone omits it, and then publishes no phone (`manifest.phoneStatus`). See **The phone's own view** in `guide/phone.md`. |
|
|
@@ -2283,6 +2351,7 @@ a capability it agrees with the login: a lead's snapshot carries `team`, nobody
|
|
|
2283
2351
|
| `phone` | The phone as the platform sees it -- the device, whether it can take a call, its own mute where observed -- from a provider whose manifest declares `phoneStatus`; required there (`snapshot.phone.required`) and forbidden otherwise (`snapshot.phone.unexpected`). Replaced whole by `phone-updated`. See **The phone's own view** in `guide/phone.md`. |
|
|
2284
2352
|
| `nextCall` | The agent's standing ask for the next call, while it stands: `since`. It needs an assignment at work to be next after (`snapshot.nextCall.idle`) and the provider clears it when that assignment ends, when a call is lined up, and when the agent withdraws it. Replaced whole by `next-call`. See **The agent's own queue** in `guide/queue.md`. |
|
|
2285
2353
|
| `linedUp` | The call waiting in the agent's own queue, while one is: the caller as shown to this audience, the queue, the caller's wait, when it was lined up, and `release` where the provider lets the agent let it go. Not a task yet. Replaced whole by `lined-up`. See **The agent's own queue** in `guide/queue.md`. |
|
|
2354
|
+
| `labels` | The signed-in customer's own words for phases, history steps and kinds of work, per language, as its admin set them in the provider. Replaced whole by `labels-updated`. Omitted where the customer set none. See **Presentation labels**. |
|
|
2286
2355
|
| `shift` | The agent's own day so far, as the provider counts it -- the same `Shift` their lead sees on the team member list, the same numbers from the same count. Replaced whole by `shift-updated`. Omitted only where the provider cannot say. See **The agent's day is on the wire**. |
|
|
2287
2356
|
| `team` | Required `TeamMembers` when the login declares `capabilities.lead` and the lead has the team feature on, `members: []` when nobody is in it. Forbidden otherwise — the login is the permission, and a lead who turned the feature off gets nothing of the team. |
|
|
2288
2357
|
|
|
@@ -2326,7 +2395,7 @@ Returns the provider's complete authoritative state at one point in time.
|
|
|
2326
2395
|
snapshot is read is held until it lands, and what happens to it then depends on what it is. A
|
|
2327
2396
|
snapshot restates state, so an event of a state-replacing kind -- `task-offered`, `task-updated`,
|
|
2328
2397
|
`task-ended`, `task-audio-started`, `task-audio-ended`, `break-state`, `team-updated`,
|
|
2329
|
-
`team-member-updated`, `team-member-removed`, `team-policies-updated`, `shift-updated`,
|
|
2398
|
+
`team-member-updated`, `team-member-removed`, `team-policies-updated`, `shift-updated`, `labels-updated`,
|
|
2330
2399
|
`contacts-updated`, `calendar-updated`, `transport-status`, `snapshot` -- is dropped, because the
|
|
2331
2400
|
snapshot accounts for it. An event that reports a transaction no snapshot carries --
|
|
2332
2401
|
`dial-outcome`, `diagnostic`, `announcement`, `queue-summary` -- is applied after the snapshot,
|
|
@@ -2892,6 +2961,11 @@ ended, a break ended, a sign-out -- and never to say nothing changed. It carries
|
|
|
2892
2961
|
the agent's lead sees for them on the team member list, so the two screens show one day. See
|
|
2893
2962
|
**The agent's day is on the wire**.
|
|
2894
2963
|
|
|
2964
|
+
### `labels-updated`
|
|
2965
|
+
|
|
2966
|
+
The customer's words, whole, replacing `Snapshot.labels`: emitted when its admin changes them, and
|
|
2967
|
+
carrying `{}` when the admin has cleared them all. See **Presentation labels**.
|
|
2968
|
+
|
|
2895
2969
|
## Utilities
|
|
2896
2970
|
|
|
2897
2971
|
### `assignmentKey(providerId, assignmentId)`
|
|
@@ -2939,6 +3013,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
|
|
|
2939
3013
|
| `validateHostReport(report)` | The agent application's own report as published to an adapter: `online`, and where there is audio, an input that is `available` with the microphone and `flowing`, or `unavailable` with a reason and the failure that says why, and an output that is `available` or `unavailable` with its failure. The harness validates whatever agent application a test hands the adapter; `stillHost(report)` builds one that never changes. |
|
|
2940
3014
|
| `validateNextCall(value)` | The agent's standing ask for the next call, on its own as `next-call` and `Snapshot.nextCall` carry it: since when, and nothing else (`nextCall.since`, `nextCall.field`). |
|
|
2941
3015
|
| `validateLinedUp(value, path?, levels?)` | A call lined up in the agent's own queue, on its own as `lined-up` and `Snapshot.linedUp` carry it: the caller under the same contact rules as a task's party, the queue's label, the waits, and `release` by presence (`linedUp.*`). |
|
|
3016
|
+
| `validateLabels(value, path?)` | The customer's words on their own, as `labels-updated` and `Snapshot.labels` carry them: renames of the protocol's phases and steps only, and every word one text per well-formed language tag (`labels.*`). |
|
|
2942
3017
|
| `validatePhoneState(value, path?, context?)` | The phone as the platform sees it, on its own as `phone-updated` and `Snapshot.phone` carry it: its status, its one active channel, its mute where a hardphone's is observed (`phone.*`). |
|
|
2943
3018
|
| `validateHostMute(mute, softphone)` | What the agent application's Mute does, stated on a softphone login and nowhere else: `stream` or `station` (`host.mute`), required where the agent application holds a microphone (`host.mute.required`) and refused where it does not (`host.mute.unexpected`). The harness holds `ConnectContext.host.mute` to it. |
|
|
2944
3019
|
| `validateLoginStore(store)` | The login's store the agent application hands every connection: an object with `get`, `set` and `delete` (`store.shape`, `store.get`, `.set`, `.delete`). The harness holds `ConnectContext.store` to it. |
|
|
@@ -3167,6 +3242,6 @@ Adapters should run the relevant scenarios against deterministic test state befo
|
|
|
3167
3242
|
|
|
3168
3243
|
How a deployment themes Omni is the agent application's concern and is specified with the agent application, not here. What
|
|
3169
3244
|
belongs in this contract is the boundary. A provider says what a control **is** through its
|
|
3170
|
-
capabilities and what its work is **called** through
|
|
3245
|
+
capabilities and what its work is **called** through the customer's `labels`; how
|
|
3171
3246
|
any of it is drawn is Omni's. A task cannot select a design language, inject a component, or
|
|
3172
3247
|
override the agent's theme and font preferences.
|