@xema/omni-protocol 0.1.121 → 0.1.122
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 +54 -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 +14 -4
- package/guide.md +102 -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, 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" | "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` 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,24 @@ 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 queued: "Queued";
|
|
2057
|
+
readonly offered: "Offered";
|
|
2058
|
+
readonly answered: "Answered";
|
|
2059
|
+
readonly held: "On hold";
|
|
2060
|
+
readonly muted: "Muted";
|
|
2061
|
+
readonly transferred: "Transferred";
|
|
2062
|
+
readonly conferenced: "Conferenced";
|
|
2063
|
+
readonly unanswered: "Unanswered";
|
|
2064
|
+
readonly "callback-requested": "Callback requested";
|
|
2065
|
+
};
|
|
2066
|
+
/**
|
|
2067
|
+
* The provider's text for the first of `languages` it has, matched as BCP 47 lookup does: the tag
|
|
2068
|
+
* whole, then with its last subtag dropped, so `hi-IN` finds `hi`. Undefined when it has none of
|
|
2069
|
+
* them, and the agent application shows its own word; never another language in their place.
|
|
2070
|
+
*/
|
|
2071
|
+
export declare function localizedText(text: LocalizedText | undefined, languages: readonly LanguageTag[]): string | undefined;
|
|
2030
2072
|
/**
|
|
2031
2073
|
* A collision-safe global task key.
|
|
2032
2074
|
*
|
|
@@ -2044,8 +2086,9 @@ export declare const assignmentKey: (providerId: string, assignmentId: Assignmen
|
|
|
2044
2086
|
*/
|
|
2045
2087
|
export declare const userKey: (providerId: string, userId: UserId) => string;
|
|
2046
2088
|
/**
|
|
2047
|
-
* Every history step somebody from the provider takes part in. `queued`
|
|
2048
|
-
* `callback-requested` is the caller's choice unless an agent arranged it, so
|
|
2089
|
+
* Every history step somebody from the provider takes part in. `arrived` and `queued` are the ones
|
|
2090
|
+
* nobody does, and `callback-requested` is the caller's choice unless an agent arranged it, so none
|
|
2091
|
+
* of them is here.
|
|
2049
2092
|
*/
|
|
2050
2093
|
export declare const HISTORY_STEPS_WITH_A_PERSON: readonly ["offered", "answered", "held", "muted", "transferred", "conferenced", "unanswered"];
|
|
2051
2094
|
/** 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", 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. `queued`
|
|
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` and `queued` are the ones
|
|
192
|
+
* 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, 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` and `queued` nobody takes part, so there is nothing to name.
|
|
999
|
+
into.require(entry.step !== "arrived" && 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,7 @@ 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: 20 },
|
|
412
413
|
{ step: "queued", at: "2026-08-21T00:59:00Z", seconds: 41 },
|
|
413
414
|
{ step: "answered", at: "2026-08-21T00:59:41Z", seconds: 312, by: "a-17" },
|
|
414
415
|
{ step: "held", at: "2026-08-21T01:02:10Z", seconds: 35, by: "a-17" },
|
|
@@ -429,9 +430,18 @@ something else, which will arrive under its own name and must not be folded in h
|
|
|
429
430
|
|
|
430
431
|
It rides in the snapshot and is replaced whole like everything else there.
|
|
431
432
|
|
|
432
|
-
Steps are `queued`, `offered`, `answered`, `held`, `muted`, `transferred`, `conferenced`,
|
|
433
|
+
Steps are `arrived`, `queued`, `offered`, `answered`, `held`, `muted`, `transferred`, `conferenced`,
|
|
433
434
|
`unanswered`, `callback-requested`, and each is defined on `TaskHistoryStep`.
|
|
434
435
|
|
|
436
|
+
**The record starts where the call started.** `arrived` is the instant the call reached the
|
|
437
|
+
provider, before any menu or queue, and its `seconds`, when the provider measured them, are how
|
|
438
|
+
long it took to move on to the next step. Nobody takes part in it, so it never has `by`. It is the
|
|
439
|
+
first step and appears once (`task.history.arrived.first`); a provider that did not see the call
|
|
440
|
+
arrive omits it rather than guessing an instant. Omni shows it as *Incoming call* unless the customer words it in
|
|
441
|
+
`labels.historySteps`, the way
|
|
442
|
+
Amazon Connect keeps an initiation time, Genesys Cloud a conversation start and NICE CXone a contact
|
|
443
|
+
start, separate from the queue.
|
|
444
|
+
|
|
435
445
|
**A callback carries the call it comes back to.** When a caller asks to be called back, or an agent
|
|
436
446
|
arranges it, the provider writes a `callback-requested` step: `by` names the agent who arranged it, and
|
|
437
447
|
is absent when the caller chose it in the queue. The callback reaches an agent as a new task, and that
|
|
@@ -465,7 +475,7 @@ hold, at the entry's `at` plus its `seconds`, and nothing is lost by not naming
|
|
|
465
475
|
`muted` entries, and a call that joined two queues -- a menu's, then this one -- has two `queued`
|
|
466
476
|
entries, one per join. Order is enforced: an entry earlier than the one before it is refused
|
|
467
477
|
(`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
|
|
478
|
+
total the provider adds: *time to offer*, from the call joining its queue to the agent's screen lighting
|
|
469
479
|
up, is the `offered` entry's `at` minus the last `queued` entry's, and the ring is outside it.
|
|
470
480
|
`queueSeconds` keeps the industry's meaning -- the whole wait until somebody answered -- and is not
|
|
471
481
|
that interval. **The second is the record's grain.** A duration is a positive whole number of
|
|
@@ -541,7 +551,7 @@ Four rules a provider has to keep:
|
|
|
541
551
|
wrongly; carrying the name here would also copy it into every task and leave it to go stale.
|
|
542
552
|
|
|
543
553
|
**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
|
|
554
|
+
`arrived` and `queued` nobody takes part, so there is nothing to name. On every other step somebody did — see
|
|
545
555
|
`HISTORY_STEPS_WITH_A_PERSON` and `historyStepExpectsAPerson()` — so an absent `by` there says
|
|
546
556
|
*this was handled and the provider cannot say by whom*. The one step that is never unattributed is
|
|
547
557
|
`muted`: the agent application has exactly one agent and the provider knows who, so `by` is required there
|
|
@@ -554,7 +564,7 @@ worse than one saying plainly it could not attribute a leg — and far better th
|
|
|
554
564
|
nothing because a single leg could not be named.
|
|
555
565
|
|
|
556
566
|
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
|
|
567
|
+
`queued` or `arrived` tells the agent nobody was involved, which is not what was said. Omni renders it as
|
|
558
568
|
*"not recorded"* in the place the name would go.
|
|
559
569
|
|
|
560
570
|
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,7 @@ type TaskAttribute = TaskAttributeBase & (
|
|
|
686
695
|
);
|
|
687
696
|
|
|
688
697
|
type HistoryStep =
|
|
698
|
+
| "arrived"
|
|
689
699
|
| "queued"
|
|
690
700
|
| "offered"
|
|
691
701
|
| "answered"
|
|
@@ -1218,7 +1228,8 @@ type ProviderEvent =
|
|
|
1218
1228
|
| { type: "shift-updated"; shift: Shift }
|
|
1219
1229
|
| { type: "phone-updated"; phone: PhoneState }
|
|
1220
1230
|
| { type: "next-call"; nextCall?: NextCallRequest }
|
|
1221
|
-
| { type: "lined-up"; linedUp?: LinedUpCall }
|
|
1231
|
+
| { type: "lined-up"; linedUp?: LinedUpCall }
|
|
1232
|
+
| { type: "labels-updated"; labels: Labels };
|
|
1222
1233
|
|
|
1223
1234
|
type ProviderEventEnvelope = {
|
|
1224
1235
|
id: string;
|
|
@@ -1351,8 +1362,8 @@ const OMNI_FAILURE_CODES = [
|
|
|
1351
1362
|
type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
|
|
1352
1363
|
```
|
|
1353
1364
|
|
|
1354
|
-
`HISTORY_STEPS_WITH_A_PERSON` is every `HistoryStep` except `queued`, which
|
|
1355
|
-
takes part in. `historyStepExpectsAPerson()` tests membership.
|
|
1365
|
+
`HISTORY_STEPS_WITH_A_PERSON` is every `HistoryStep` except `arrived` and `queued`, which nobody
|
|
1366
|
+
takes part in, and `callback-requested`, which the caller may choose alone. `historyStepExpectsAPerson()` tests membership.
|
|
1356
1367
|
|
|
1357
1368
|
### Failure and validation
|
|
1358
1369
|
|
|
@@ -1373,25 +1384,82 @@ interface ProtocolViolation {
|
|
|
1373
1384
|
|
|
1374
1385
|
## Presentation labels
|
|
1375
1386
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1387
|
+
**Each customer's admin chooses the words its agents see, in each language it serves**, in the
|
|
1388
|
+
provider, and the provider publishes them for the signed-in customer as `Snapshot.labels`,
|
|
1389
|
+
replaced whole by `labels-updated` when the admin changes them. Customer A's agents read *New call*
|
|
1390
|
+
where customer B's read *Entered*, on the same adapter, and a change reaches agents without an
|
|
1391
|
+
adapter release. That is how Genesys Cloud loads an organisation's presence names at sign-in and
|
|
1392
|
+
pushes the change when an admin edits one, and how Amazon Connect and NICE CXone keep agent state
|
|
1393
|
+
names per instance or business unit. A manifest is fixed when the adapter ships, so it carries no
|
|
1394
|
+
labels (`manifest.labels.moved`). `labels` is omitted, or `labels-updated` carries `{}`, when the
|
|
1395
|
+
customer set nothing, and the agent application shows its own words throughout.
|
|
1396
|
+
|
|
1397
|
+
```ts
|
|
1398
|
+
labels: {
|
|
1399
|
+
phases: { "in-progress": { en: "On Call", hi: "कॉल पर" } },
|
|
1400
|
+
historySteps: { arrived: { en: "New call", hi: "नई कॉल" }, queued: { en: "Waiting" } },
|
|
1401
|
+
taskTypes: { Support: { singular: { en: "Support call" }, plural: { en: "Support calls" }, referenceLabel: { en: "Case" } } },
|
|
1402
|
+
}
|
|
1403
|
+
```
|
|
1404
|
+
|
|
1405
|
+
`validateLabels(value, path?)` checks one on its own.
|
|
1406
|
+
|
|
1407
|
+
**Every word is per language.** Every label is a
|
|
1408
|
+
`LocalizedText`: one text per BCP 47 language tag, as `{ en: "On Call", hi: "कॉल पर" }`. The agent
|
|
1409
|
+
application finds the agent's language with `localizedText(text, languages)`, which tries each of
|
|
1410
|
+
the agent's languages in order, the tag whole and then without its last subtag, so `hi-IN` finds
|
|
1411
|
+
`hi`. When the provider wrote none of them, it returns nothing and the agent application shows its
|
|
1412
|
+
own word; it never shows another language in their place. A single string is the former shape and
|
|
1413
|
+
is refused (`labels.phases.label`, `labels.historySteps.label`,
|
|
1414
|
+
`labels.taskTypes.singular`), as are an empty text (`….text`), a key that is not a language tag
|
|
1415
|
+
(`….language`) and one language given twice in different case (`….language.unique`). This is how
|
|
1416
|
+
Genesys Cloud words its presence definitions and NICE CXone its agent states: the value is the
|
|
1417
|
+
platform's, and the words are per language.
|
|
1418
|
+
|
|
1419
|
+
The values underneath never change: a customer renames what its agents see, so a lead's counts by
|
|
1420
|
+
phase or by step still add up.
|
|
1378
1421
|
|
|
1379
1422
|
### Renaming a phase
|
|
1380
1423
|
|
|
1381
|
-
`
|
|
1382
|
-
phase or removes one.
|
|
1424
|
+
`labels.phases` renames the canonical `TaskPhase` values for the agent. It never adds a
|
|
1425
|
+
phase or removes one (`labels.phases.phase`).
|
|
1383
1426
|
|
|
1384
1427
|
`TaskPhaseLabels` is `Partial` so that renaming one phase keeps the default wording for the rest —
|
|
1385
|
-
|
|
1428
|
+
a customer changing "On Call" does not restate the other five.
|
|
1386
1429
|
|
|
1387
|
-
**A task is labelled by the provider that owns it**, from that provider's
|
|
1430
|
+
**A task is labelled by the provider that owns it**, from that provider's `labels` merged over its
|
|
1388
1431
|
channel defaults. Two adapters may word the same phase differently and each is right about its own
|
|
1389
1432
|
tasks: a call reads as the voice platform names it while a chat beside it reads as its own does.
|
|
1390
1433
|
How Omni words a view spanning several providers is its own presentation problem, not a provider's.
|
|
1391
1434
|
|
|
1435
|
+
### Naming the history steps
|
|
1436
|
+
|
|
1437
|
+
`labels.historySteps` words the `HistoryStep` values the way `labels.phases` words phases: per
|
|
1438
|
+
step, per language, and partial, so a customer that calls `arrived` *Entered* restates nothing
|
|
1439
|
+
else. It never adds a step; a provider's own stages that the protocol has no step for are not
|
|
1440
|
+
labelled into existence (`labels.historySteps.step`).
|
|
1441
|
+
|
|
1442
|
+
`DEFAULT_HISTORY_STEP_LABELS` is what Omni shows when the customer words a step in none of the
|
|
1443
|
+
agent's languages:
|
|
1444
|
+
|
|
1445
|
+
```ts
|
|
1446
|
+
const DEFAULT_HISTORY_STEP_LABELS = {
|
|
1447
|
+
arrived: "Incoming call",
|
|
1448
|
+
queued: "Queued",
|
|
1449
|
+
offered: "Offered",
|
|
1450
|
+
answered: "Answered",
|
|
1451
|
+
held: "On hold",
|
|
1452
|
+
muted: "Muted",
|
|
1453
|
+
transferred: "Transferred",
|
|
1454
|
+
conferenced: "Conferenced",
|
|
1455
|
+
unanswered: "Unanswered",
|
|
1456
|
+
"callback-requested": "Callback requested",
|
|
1457
|
+
} as const satisfies Readonly<Record<HistoryStep, string>>;
|
|
1458
|
+
```
|
|
1459
|
+
|
|
1392
1460
|
### `DEFAULT_TASK_PHASE_LABELS`
|
|
1393
1461
|
|
|
1394
|
-
What Omni shows when
|
|
1462
|
+
What Omni shows when the customer renames nothing.
|
|
1395
1463
|
|
|
1396
1464
|
```ts
|
|
1397
1465
|
const DEFAULT_TASK_PHASE_LABELS = {
|
|
@@ -1426,14 +1494,14 @@ const DEFAULT_TASK_PHASE_LABELS = {
|
|
|
1426
1494
|
|
|
1427
1495
|
### Naming a kind of work
|
|
1428
1496
|
|
|
1429
|
-
`TaskTypePresentation` says what one kind of work is called
|
|
1430
|
-
**replaces the channel default outright** rather than merging.
|
|
1497
|
+
`TaskTypePresentation` says what one kind of work is called, in `labels.taskTypes`. Unlike
|
|
1498
|
+
`labels.phases`, an entry **replaces the channel default outright** rather than merging.
|
|
1431
1499
|
`singular` and `plural` are required together because pluralisation is not mechanical, and a
|
|
1432
1500
|
half-supplied entry would leave Omni pairing one provider's noun with another's plural.
|
|
1433
1501
|
|
|
1434
1502
|
### `DEFAULT_TASK_TYPE_PRESENTATION`
|
|
1435
1503
|
|
|
1436
|
-
The per-channel fallback, used for any `taskType` the
|
|
1504
|
+
The per-channel fallback, used for any `taskType` the customer does not name.
|
|
1437
1505
|
|
|
1438
1506
|
```ts
|
|
1439
1507
|
const DEFAULT_TASK_TYPE_PRESENTATION = {
|
|
@@ -1453,21 +1521,21 @@ const DEFAULT_TASK_TYPE_PRESENTATION = {
|
|
|
1453
1521
|
referenceLabel: "Email ID",
|
|
1454
1522
|
},
|
|
1455
1523
|
} as const satisfies Readonly<
|
|
1456
|
-
Record<"voice" | "chat" | "email",
|
|
1524
|
+
Record<"voice" | "chat" | "email", { singular: string; plural: string; referenceLabel: string }>
|
|
1457
1525
|
>;
|
|
1458
1526
|
```
|
|
1459
1527
|
|
|
1460
1528
|
### Naming task types
|
|
1461
1529
|
|
|
1462
1530
|
`channel` is too coarse for agent-facing names. A WhatsApp call and a PSTN call are both `voice`,
|
|
1463
|
-
but the
|
|
1531
|
+
but the customer may name them differently:
|
|
1464
1532
|
|
|
1465
1533
|
```ts
|
|
1466
|
-
const
|
|
1534
|
+
const taskTypes = {
|
|
1467
1535
|
WhatsApp: {
|
|
1468
|
-
singular: "Conversation",
|
|
1469
|
-
plural: "Conversations",
|
|
1470
|
-
referenceLabel: "Conversation ID",
|
|
1536
|
+
singular: { en: "Conversation", hi: "बातचीत" },
|
|
1537
|
+
plural: { en: "Conversations", hi: "बातचीतें" },
|
|
1538
|
+
referenceLabel: { en: "Conversation ID" },
|
|
1471
1539
|
},
|
|
1472
1540
|
} satisfies Record<string, TaskTypePresentation>;
|
|
1473
1541
|
```
|
|
@@ -1710,8 +1778,6 @@ compile time.
|
|
|
1710
1778
|
| `supportedProtocolVersions` | Required non-empty list of versions this adapter can speak. Must share at least one with Omni. |
|
|
1711
1779
|
| `authenticationMethods` | Required non-empty list of supported login methods. |
|
|
1712
1780
|
| `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
1781
|
| `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
1782
|
| `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
1783
|
| `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 +2349,7 @@ a capability it agrees with the login: a lead's snapshot carries `team`, nobody
|
|
|
2283
2349
|
| `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
2350
|
| `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
2351
|
| `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`. |
|
|
2352
|
+
| `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
2353
|
| `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
2354
|
| `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
2355
|
|
|
@@ -2326,7 +2393,7 @@ Returns the provider's complete authoritative state at one point in time.
|
|
|
2326
2393
|
snapshot is read is held until it lands, and what happens to it then depends on what it is. A
|
|
2327
2394
|
snapshot restates state, so an event of a state-replacing kind -- `task-offered`, `task-updated`,
|
|
2328
2395
|
`task-ended`, `task-audio-started`, `task-audio-ended`, `break-state`, `team-updated`,
|
|
2329
|
-
`team-member-updated`, `team-member-removed`, `team-policies-updated`, `shift-updated`,
|
|
2396
|
+
`team-member-updated`, `team-member-removed`, `team-policies-updated`, `shift-updated`, `labels-updated`,
|
|
2330
2397
|
`contacts-updated`, `calendar-updated`, `transport-status`, `snapshot` -- is dropped, because the
|
|
2331
2398
|
snapshot accounts for it. An event that reports a transaction no snapshot carries --
|
|
2332
2399
|
`dial-outcome`, `diagnostic`, `announcement`, `queue-summary` -- is applied after the snapshot,
|
|
@@ -2892,6 +2959,11 @@ ended, a break ended, a sign-out -- and never to say nothing changed. It carries
|
|
|
2892
2959
|
the agent's lead sees for them on the team member list, so the two screens show one day. See
|
|
2893
2960
|
**The agent's day is on the wire**.
|
|
2894
2961
|
|
|
2962
|
+
### `labels-updated`
|
|
2963
|
+
|
|
2964
|
+
The customer's words, whole, replacing `Snapshot.labels`: emitted when its admin changes them, and
|
|
2965
|
+
carrying `{}` when the admin has cleared them all. See **Presentation labels**.
|
|
2966
|
+
|
|
2895
2967
|
## Utilities
|
|
2896
2968
|
|
|
2897
2969
|
### `assignmentKey(providerId, assignmentId)`
|
|
@@ -2939,6 +3011,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
|
|
|
2939
3011
|
| `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
3012
|
| `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
3013
|
| `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.*`). |
|
|
3014
|
+
| `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
3015
|
| `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
3016
|
| `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
3017
|
| `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 +3240,6 @@ Adapters should run the relevant scenarios against deterministic test state befo
|
|
|
3167
3240
|
|
|
3168
3241
|
How a deployment themes Omni is the agent application's concern and is specified with the agent application, not here. What
|
|
3169
3242
|
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
|
|
3243
|
+
capabilities and what its work is **called** through the customer's `labels`; how
|
|
3171
3244
|
any of it is drawn is Omni's. A task cannot select a design language, inject a component, or
|
|
3172
3245
|
override the agent's theme and font preferences.
|