@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 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
- export type TaskPhaseLabels = Readonly<Partial<Record<TaskPhase, string>>>;
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: string;
183
- plural: string;
184
- referenceLabel?: string;
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. `queued` is the one nobody does, and
2048
- * `callback-requested` is the caller's choice unless an agent arranged it, so neither is here.
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. `queued` is the one nobody does, and
169
- * `callback-requested` is the caller's choice unless an agent arranged it, so neither is here.
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();
@@ -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;
@@ -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
- if (manifest.phaseLabels !== undefined) {
521
- if (!isPlainObject(manifest.phaseLabels)) {
522
- into.add("manifest.phaseLabels.shape", `${path}.phaseLabels`, "phaseLabels must be an object when present");
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
- if (manifest.taskTypePresentation !== undefined) {
570
- if (!isPlainObject(manifest.taskTypePresentation)) {
571
- into.add("manifest.taskTypePresentation.shape", `${path}.taskTypePresentation`, "taskTypePresentation must be an object when present");
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
- else {
574
- for (const [taskType, presentation] of Object.entries(manifest.taskTypePresentation)) {
575
- const at = `${path}.taskTypePresentation.${taskType}`;
576
- if (!isPlainObject(presentation)) {
577
- into.add("manifest.taskTypePresentation.entry", at, "each presentation must be an object");
578
- continue;
579
- }
580
- into.filled(presentation.singular, "manifest.taskTypePresentation.singular", `${at}.singular`, "a presentation needs a singular name");
581
- into.filled(presentation.plural, "manifest.taskTypePresentation.plural", `${at}.plural`, "a presentation needs a plural name");
582
- if (presentation.referenceLabel !== undefined) {
583
- into.filled(presentation.referenceLabel, "manifest.taskTypePresentation.referenceLabel", `${at}.referenceLabel`, "referenceLabel must not be empty when present");
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
- return into.violations;
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`, "a queued step names nobody");
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's arrival to the agent's screen lighting
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 TaskPhaseLabels = Readonly<Partial<Record<TaskPhase, string>>>;
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: string;
266
- plural: string;
267
- referenceLabel?: string;
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 `queued`, which is the one nobody
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
- Adapters may supply static display labels for canonical protocol values. Labels affect presentation
1377
- only and cannot vary by connection, task, snapshot, or event.
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
- `manifest.phaseLabels` renames the canonical `TaskPhase` values for the agent. It never adds a
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
- an adapter changing "On Call" does not restate the other five.
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 manifest merged over its
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 an adapter overrides nothing.
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. Unlike `phaseLabels`, an entry
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 adapter does not name.
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", TaskTypePresentation>
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 provider may name them differently through static manifest metadata:
1533
+ but the customer may name them differently:
1464
1534
 
1465
1535
  ```ts
1466
- const taskTypePresentation = {
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 `phaseLabels` and `taskTypePresentation`; how
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.121",
3
+ "version": "0.1.123",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",