@xema/omni-protocol 0.1.44 → 0.1.45
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/README.md +3 -3
- package/dist/index.d.ts +24 -22
- package/dist/index.js +1 -1
- package/dist/validation.js +13 -21
- package/guide.md +47 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,12 +99,12 @@ const task = {
|
|
|
99
99
|
|
|
100
100
|
**Switch on `command.action` with no `default`.** A `default` turns a future action into whatever
|
|
101
101
|
the default does, silently; naming every arm means dropping one fails the build — indirectly, as a
|
|
102
|
-
missing return — and keeps the narrowing that lets you read `
|
|
102
|
+
missing return — and keeps the narrowing that lets you read `destinationId` without a cast.
|
|
103
103
|
|
|
104
104
|
```ts
|
|
105
105
|
switch (command.action) {
|
|
106
|
-
case "cold": return coldTransfer(command.
|
|
107
|
-
case "warm": return warmTransfer(command.
|
|
106
|
+
case "cold": return coldTransfer(command.destinationId);
|
|
107
|
+
case "warm": return warmTransfer(command.destinationId);
|
|
108
108
|
case "complete": return completeWarmTransfer();
|
|
109
109
|
case "cancel": return cancelWarmTransfer();
|
|
110
110
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -409,16 +409,18 @@ export interface DispositionRules {
|
|
|
409
409
|
notes?: "required" | "optional" | "none";
|
|
410
410
|
codes?: DispositionCode[];
|
|
411
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* One item the queue configured for the agent to send a contact to: a button or a menu item. The
|
|
414
|
+
* protocol does not say what it does -- a queue, a menu, a line -- the provider executes it when a
|
|
415
|
+
* command names its `id`. Never a named agent: who takes a contact next is the queue's decision.
|
|
416
|
+
*/
|
|
412
417
|
export interface Destination {
|
|
413
418
|
id: string;
|
|
414
419
|
label: string;
|
|
415
|
-
address: string;
|
|
416
|
-
/** `agent` here is a routing target, not the person signed in. */
|
|
417
|
-
kind: "queue" | "agent" | "external";
|
|
418
420
|
}
|
|
421
|
+
/** The items a transfer or conference control offers. Nothing is typed; the agent picks one. */
|
|
419
422
|
export interface DestinationDirectory {
|
|
420
|
-
destinations
|
|
421
|
-
allowManualEntry: boolean;
|
|
423
|
+
destinations: Destination[];
|
|
422
424
|
}
|
|
423
425
|
export interface CustomCapability {
|
|
424
426
|
id: string;
|
|
@@ -451,12 +453,12 @@ export type TaskCapabilities<C extends Channel = Channel> = C extends "voice" ?
|
|
|
451
453
|
agentDisconnect?: Lockable<true>;
|
|
452
454
|
/** Connect back to the party while `completing`, whoever placed the call; the task returns to `in-progress`. */
|
|
453
455
|
connectBack?: Lockable<true>;
|
|
454
|
-
coldTransfer?: Lockable<
|
|
456
|
+
coldTransfer?: Lockable<DestinationDirectory>;
|
|
455
457
|
/** Park the customer and call a destination first; then `complete` or `cancel`. */
|
|
456
|
-
warmTransfer?: Lockable<
|
|
458
|
+
warmTransfer?: Lockable<DestinationDirectory>;
|
|
457
459
|
/** Ask a lead to join this call, with a note. The lead's decision arrives on `Task.leadAssist`. */
|
|
458
460
|
leadAssist?: Lockable<true>;
|
|
459
|
-
conference?: Lockable<
|
|
461
|
+
conference?: Lockable<DestinationDirectory>;
|
|
460
462
|
recording?: Lockable<true>;
|
|
461
463
|
} : C extends "chat" ? SharedTaskCapabilities & {
|
|
462
464
|
decline?: Lockable<true>;
|
|
@@ -561,10 +563,10 @@ export interface TaskHandlingStep {
|
|
|
561
563
|
at: IsoTimestamp;
|
|
562
564
|
/**
|
|
563
565
|
* On a step that dialled -- `transferred`, `conferenced`, `unanswered` -- the host's identity for
|
|
564
|
-
* that dial when a host placed it, and the
|
|
566
|
+
* that dial when a host placed it, and the directory item it went to. Neither belongs on any other step.
|
|
565
567
|
*/
|
|
566
568
|
dialId?: DialId;
|
|
567
|
-
|
|
569
|
+
destinationId?: string;
|
|
568
570
|
/**
|
|
569
571
|
* Reported, never derived. An entry may be written while its leg is still running, so there is
|
|
570
572
|
* no end to subtract from -- and omitted is the honest report of that, where nought would
|
|
@@ -616,7 +618,7 @@ export type OnCall = {
|
|
|
616
618
|
userId: UserId;
|
|
617
619
|
} | {
|
|
618
620
|
role: "consulted" | "conferenced";
|
|
619
|
-
|
|
621
|
+
destinationId: string;
|
|
620
622
|
stage: OnCallStage;
|
|
621
623
|
dialId?: DialId;
|
|
622
624
|
label?: string;
|
|
@@ -751,7 +753,7 @@ export type TaskOutcome = {
|
|
|
751
753
|
by: "agent" | "provider";
|
|
752
754
|
} | {
|
|
753
755
|
type: "transferred";
|
|
754
|
-
|
|
756
|
+
destinationId?: string;
|
|
755
757
|
} | {
|
|
756
758
|
type: "cancelled";
|
|
757
759
|
reason?: string;
|
|
@@ -802,19 +804,19 @@ export type VoiceTaskCommand = {
|
|
|
802
804
|
type: "connect-back";
|
|
803
805
|
dialId: DialId;
|
|
804
806
|
}
|
|
805
|
-
/** Cold: hand the customer to `
|
|
807
|
+
/** Cold: hand the customer to the directory item `destinationId` with nobody spoken to first. Gated by `coldTransfer`. */
|
|
806
808
|
| {
|
|
807
809
|
type: "transfer";
|
|
808
810
|
action: "cold";
|
|
809
811
|
dialId: DialId;
|
|
810
|
-
|
|
812
|
+
destinationId: string;
|
|
811
813
|
}
|
|
812
|
-
/** Warm: park the customer and call `
|
|
814
|
+
/** Warm: park the customer and call the directory item `destinationId` first. Gated by `warmTransfer`. */
|
|
813
815
|
| {
|
|
814
816
|
type: "transfer";
|
|
815
817
|
action: "warm";
|
|
816
818
|
dialId: DialId;
|
|
817
|
-
|
|
819
|
+
destinationId: string;
|
|
818
820
|
}
|
|
819
821
|
/** Hand the customer to the consulted destination and leave. Needs a `consulted` entry on `Task.onCall`. */
|
|
820
822
|
| {
|
|
@@ -847,18 +849,18 @@ export type VoiceTaskCommand = {
|
|
|
847
849
|
type: "lead-assist";
|
|
848
850
|
action: "leave";
|
|
849
851
|
}
|
|
850
|
-
/** Dial `
|
|
852
|
+
/** Dial the directory item `destinationId` into the call. Gated by `conference`. */
|
|
851
853
|
| {
|
|
852
854
|
type: "conference";
|
|
853
855
|
action: "add";
|
|
854
856
|
dialId: DialId;
|
|
855
|
-
|
|
857
|
+
destinationId: string;
|
|
856
858
|
}
|
|
857
|
-
/** Drop the `conferenced` entry on `Task.onCall` with this
|
|
859
|
+
/** Drop the `conferenced` entry on `Task.onCall` with this `destinationId`; while it still rings, this calls the dial off. */
|
|
858
860
|
| {
|
|
859
861
|
type: "conference";
|
|
860
862
|
action: "remove";
|
|
861
|
-
|
|
863
|
+
destinationId: string;
|
|
862
864
|
} | {
|
|
863
865
|
type: "recording";
|
|
864
866
|
action: "start" | "pause" | "resume" | "stop";
|
|
@@ -1296,7 +1298,7 @@ export type ProviderEvent<C extends Channel = Channel> = {
|
|
|
1296
1298
|
dialId: DialId;
|
|
1297
1299
|
outcome: DialOutcome;
|
|
1298
1300
|
taskId?: TaskId;
|
|
1299
|
-
|
|
1301
|
+
destinationId?: string;
|
|
1300
1302
|
reason?: string;
|
|
1301
1303
|
} | {
|
|
1302
1304
|
type: "announcement";
|
|
@@ -1443,7 +1445,7 @@ export declare const taskKey: (providerId: string, taskId: TaskId) => string;
|
|
|
1443
1445
|
export declare const userKey: (providerId: string, userId: UserId) => string;
|
|
1444
1446
|
/** Every handling step somebody takes part in. `queued` is the one nobody does. */
|
|
1445
1447
|
export declare const HANDLING_STEPS_WITH_A_PERSON: readonly ["offered", "answered", "held", "muted", "transferred", "conferenced", "unanswered"];
|
|
1446
|
-
/** The steps a dial writes, and so the only ones that carry a `dialId` and a `
|
|
1448
|
+
/** The steps a dial writes, and so the only ones that carry a `dialId` and a `destinationId`. */
|
|
1447
1449
|
export declare const HANDLING_STEPS_THAT_DIAL: readonly ["transferred", "conferenced", "unanswered"];
|
|
1448
1450
|
export declare const handlingStepDials: (step: HandlingStep) => boolean;
|
|
1449
1451
|
/** Whether an absent `by` means "could not attribute" rather than "nobody was involved". */
|
package/dist/index.js
CHANGED
|
@@ -157,7 +157,7 @@ export const userKey = (providerId, userId) => `${encodeURIComponent(providerId)
|
|
|
157
157
|
export const HANDLING_STEPS_WITH_A_PERSON = [
|
|
158
158
|
"offered", "answered", "held", "muted", "transferred", "conferenced", "unanswered",
|
|
159
159
|
];
|
|
160
|
-
/** The steps a dial writes, and so the only ones that carry a `dialId` and a `
|
|
160
|
+
/** The steps a dial writes, and so the only ones that carry a `dialId` and a `destinationId`. */
|
|
161
161
|
export const HANDLING_STEPS_THAT_DIAL = ["transferred", "conferenced", "unanswered"];
|
|
162
162
|
export const handlingStepDials = (step) => HANDLING_STEPS_THAT_DIAL.includes(step);
|
|
163
163
|
// Pinned both ways: a step added to `HandlingStep` has to be placed here, and a step listed here
|
package/dist/validation.js
CHANGED
|
@@ -58,7 +58,6 @@ const TEAM_AVAILABILITIES = membersOf({
|
|
|
58
58
|
const HANDLING_STEPS = membersOf({
|
|
59
59
|
queued: true, offered: true, answered: true, held: true, muted: true, transferred: true, conferenced: true, unanswered: true,
|
|
60
60
|
});
|
|
61
|
-
const DESTINATION_KINDS = membersOf({ queue: true, agent: true, external: true });
|
|
62
61
|
const CUSTOM_UI_CONTROLS = membersOf({ button: true, toggle: true, "menu-item": true });
|
|
63
62
|
const CUSTOM_UI_PLACEMENTS = membersOf({ primary: true, secondary: true, overflow: true });
|
|
64
63
|
const NOTES_RULES = membersOf({ required: true, optional: true, none: true });
|
|
@@ -418,23 +417,18 @@ export function validateManifest(manifest, path = "manifest") {
|
|
|
418
417
|
// Task.
|
|
419
418
|
// ---------------------------------------------------------------------------
|
|
420
419
|
function validateDestinationDirectory(value, path, into) {
|
|
421
|
-
if (value === true)
|
|
422
|
-
return;
|
|
423
420
|
if (!isPlainObject(value)) {
|
|
424
|
-
into.add("task.destinations.shape", path, "
|
|
421
|
+
into.add("task.destinations.shape", path, "a transfer or conference control carries the directory it offers: { destinations: [...] }");
|
|
425
422
|
return;
|
|
426
423
|
}
|
|
427
|
-
into.require(typeof value.allowManualEntry === "boolean", "task.destinations.allowManualEntry", `${path}.allowManualEntry`, "a directory must say whether manual entry is allowed");
|
|
428
|
-
// A directory with nothing in it and no typing is a control with nothing to offer.
|
|
429
|
-
if (value.allowManualEntry === false && !(Array.isArray(value.destinations) && value.destinations.length > 0)) {
|
|
430
|
-
into.add("task.destinations.offer", path, "a directory with no destinations must allow manual entry, or the control has nothing to offer");
|
|
431
|
-
}
|
|
432
|
-
if (value.destinations === undefined)
|
|
433
|
-
return;
|
|
434
424
|
if (!Array.isArray(value.destinations)) {
|
|
435
|
-
into.add("task.destinations.list", `${path}.destinations`, "destinations must be an array
|
|
425
|
+
into.add("task.destinations.list", `${path}.destinations`, "destinations must be an array");
|
|
436
426
|
return;
|
|
437
427
|
}
|
|
428
|
+
// Nothing is typed, so a directory with nothing in it is a control with nothing to offer.
|
|
429
|
+
if (value.destinations.length === 0) {
|
|
430
|
+
into.add("task.destinations.offer", path, "a directory offers at least one item; with none, omit the capability");
|
|
431
|
+
}
|
|
438
432
|
const seen = new Set();
|
|
439
433
|
value.destinations.forEach((destination, index) => {
|
|
440
434
|
const at = `${path}.destinations[${index}]`;
|
|
@@ -448,8 +442,6 @@ function validateDestinationDirectory(value, path, into) {
|
|
|
448
442
|
seen.add(destination.id);
|
|
449
443
|
}
|
|
450
444
|
into.filled(destination.label, "task.destination.label", `${at}.label`, "a destination needs a label");
|
|
451
|
-
into.filled(destination.address, "task.destination.address", `${at}.address`, "a destination needs an address");
|
|
452
|
-
into.oneOf(destination.kind, DESTINATION_KINDS, "task.destination.kind", `${at}.kind`);
|
|
453
445
|
});
|
|
454
446
|
}
|
|
455
447
|
function validateDispositions(value, path, into) {
|
|
@@ -724,7 +716,7 @@ function validateHandlingHistory(value, path, into) {
|
|
|
724
716
|
}
|
|
725
717
|
// A step that dialled says where to and, when a host placed it, which dial; no other step dialled.
|
|
726
718
|
const dialled = typeof entry.step === "string" && HANDLING_STEPS_THAT_DIAL.includes(entry.step);
|
|
727
|
-
for (const field of ["dialId", "
|
|
719
|
+
for (const field of ["dialId", "destinationId"]) {
|
|
728
720
|
if (entry[field] === undefined)
|
|
729
721
|
continue;
|
|
730
722
|
if (into.filled(entry[field], `task.handlingHistory.${field}`, `${at}.${field}`, `${field} must not be empty when present`)) {
|
|
@@ -780,7 +772,7 @@ function validateOnCall(value, channel, path, into) {
|
|
|
780
772
|
into.require(entry.userId === undefined, "task.onCall.userId.unexpected", `${at}.userId`, `a ${role} is not named by a user id`);
|
|
781
773
|
}
|
|
782
774
|
if (role === "consulted" || role === "conferenced") {
|
|
783
|
-
into.filled(entry.
|
|
775
|
+
into.filled(entry.destinationId, "task.onCall.destinationId", `${at}.destinationId`, `a ${role} entry names the directory item that was dialled`);
|
|
784
776
|
// Stated, so a snapshot says who is present without anyone having seen the dial's outcome.
|
|
785
777
|
if (into.oneOf(entry.stage, ON_CALL_STAGES, "task.onCall.stage", `${at}.stage`)) {
|
|
786
778
|
into.require(!(entry.stage === "ringing" && entry.held === true), "task.onCall.held.ringing", `${at}.held`, "nobody ringing is held; held belongs to somebody who has joined");
|
|
@@ -791,7 +783,7 @@ function validateOnCall(value, channel, path, into) {
|
|
|
791
783
|
into.filled(entry.label, "task.onCall.label", `${at}.label`, "a label must not be empty when present");
|
|
792
784
|
}
|
|
793
785
|
else {
|
|
794
|
-
for (const field of ["
|
|
786
|
+
for (const field of ["destinationId", "dialId", "label", "stage"]) {
|
|
795
787
|
into.require(entry[field] === undefined, `task.onCall.${field}.unexpected`, `${at}.${field}`, `a ${role} was dialled from nowhere; ${field} belongs on consulted or conferenced`);
|
|
796
788
|
}
|
|
797
789
|
}
|
|
@@ -1297,8 +1289,8 @@ function validateTaskOutcome(value, path, into) {
|
|
|
1297
1289
|
into.oneOf(value.by, COMPLETED_BY, "event.taskEnded.outcome.completed", `${path}.by`);
|
|
1298
1290
|
break;
|
|
1299
1291
|
case "transferred":
|
|
1300
|
-
if (value.
|
|
1301
|
-
into.filled(value.
|
|
1292
|
+
if (value.destinationId !== undefined) {
|
|
1293
|
+
into.filled(value.destinationId, "event.taskEnded.outcome.transferred", `${path}.destinationId`, "a destinationId must not be empty when present");
|
|
1302
1294
|
}
|
|
1303
1295
|
break;
|
|
1304
1296
|
case "cancelled":
|
|
@@ -1433,8 +1425,8 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
|
|
|
1433
1425
|
}
|
|
1434
1426
|
if (event.taskId !== undefined)
|
|
1435
1427
|
into.require(isTaskId(event.taskId), "event.dialOutcome.taskId", `${at}.taskId`, "taskId must be a task id when present");
|
|
1436
|
-
if (event.
|
|
1437
|
-
into.filled(event.
|
|
1428
|
+
if (event.destinationId !== undefined)
|
|
1429
|
+
into.filled(event.destinationId, "event.dialOutcome.destinationId", `${at}.destinationId`, "a destinationId must not be empty when present");
|
|
1438
1430
|
if (event.reason !== undefined)
|
|
1439
1431
|
into.filled(event.reason, "event.dialOutcome.reason", `${at}.reason`, "a reason must not be empty when present");
|
|
1440
1432
|
break;
|
package/guide.md
CHANGED
|
@@ -20,7 +20,7 @@ are used precisely throughout and mean nothing looser here.
|
|
|
20
20
|
| **Host** | Omni, named that way where the contrast with a provider is the point — *the host carries the audio*, *host-side input*. |
|
|
21
21
|
| **Provider** | One independently connected external system: a voice platform, a chat platform, a mail platform. |
|
|
22
22
|
| **Adapter** | The package implementing this contract for one provider. One adapter is one provider, so the words are often interchangeable; *provider* names the system, *adapter* the code speaking for it. |
|
|
23
|
-
| **Agent** | The person signed in and taking work.
|
|
23
|
+
| **Agent** | The person signed in and taking work. On `onCall`, `agent` is a person on the call by user id; a transfer never names one, since a contact goes back to a queue. |
|
|
24
24
|
| **Lead** | An agent whose login declares `capabilities.team`. The provider publishes a `TeamRoster` to them and to nobody else: **the login is the permission**. |
|
|
25
25
|
| **Provisioning** | Omni-side policy about this agent, configured outside the protocol and never sent to a provider. It gates whether an offer may be rejected, whether the agent goes ready on login, and whether tasks are auto-accepted. Where a capability and provisioning disagree, the stricter wins. |
|
|
26
26
|
| **Task** | One unit of assigned work — a call, a chat, a mail. |
|
|
@@ -447,13 +447,10 @@ type DispositionRules = {
|
|
|
447
447
|
type Destination = {
|
|
448
448
|
id: string;
|
|
449
449
|
label: string;
|
|
450
|
-
address: string;
|
|
451
|
-
kind: "queue" | "agent" | "external";
|
|
452
450
|
};
|
|
453
451
|
|
|
454
452
|
type DestinationDirectory = {
|
|
455
|
-
destinations
|
|
456
|
-
allowManualEntry: boolean;
|
|
453
|
+
destinations: Destination[];
|
|
457
454
|
};
|
|
458
455
|
|
|
459
456
|
type CustomCapability = {
|
|
@@ -481,10 +478,10 @@ type TaskCapabilities<C extends Channel = Channel> =
|
|
|
481
478
|
hold?: Lockable<true>;
|
|
482
479
|
agentDisconnect?: Lockable<true>;
|
|
483
480
|
connectBack?: Lockable<true>;
|
|
484
|
-
coldTransfer?: Lockable<
|
|
485
|
-
warmTransfer?: Lockable<
|
|
481
|
+
coldTransfer?: Lockable<DestinationDirectory>;
|
|
482
|
+
warmTransfer?: Lockable<DestinationDirectory>;
|
|
486
483
|
leadAssist?: Lockable<true>;
|
|
487
|
-
conference?: Lockable<
|
|
484
|
+
conference?: Lockable<DestinationDirectory>;
|
|
488
485
|
recording?: Lockable<true>;
|
|
489
486
|
}
|
|
490
487
|
: C extends "chat"
|
|
@@ -582,7 +579,7 @@ type TaskHandlingStep = {
|
|
|
582
579
|
step: HandlingStep;
|
|
583
580
|
at: IsoTimestamp;
|
|
584
581
|
dialId?: DialId;
|
|
585
|
-
|
|
582
|
+
destinationId?: string;
|
|
586
583
|
seconds?: DurationSeconds;
|
|
587
584
|
by?: UserId;
|
|
588
585
|
};
|
|
@@ -598,7 +595,7 @@ type OnCallStage = "ringing" | "joined";
|
|
|
598
595
|
type OnCall = { since: IsoTimestamp; held?: true } & (
|
|
599
596
|
| { role: "party" }
|
|
600
597
|
| { role: "agent"; userId: UserId }
|
|
601
|
-
| { role: "consulted" | "conferenced";
|
|
598
|
+
| { role: "consulted" | "conferenced"; destinationId: string; stage: OnCallStage; dialId?: DialId; label?: string }
|
|
602
599
|
);
|
|
603
600
|
|
|
604
601
|
type TaskLeadAssist = {
|
|
@@ -667,7 +664,7 @@ type AcceptanceMode =
|
|
|
667
664
|
|
|
668
665
|
type TaskOutcome =
|
|
669
666
|
| { type: "completed"; by: "agent" | "provider" }
|
|
670
|
-
| { type: "transferred";
|
|
667
|
+
| { type: "transferred"; destinationId?: string }
|
|
671
668
|
| { type: "cancelled"; reason?: string }
|
|
672
669
|
| { type: "expired"; phase: "pending" | "confirmed" | "preview" }
|
|
673
670
|
| { type: "left" }
|
|
@@ -711,16 +708,16 @@ type VoiceTaskCommand =
|
|
|
711
708
|
| { type: "resume" }
|
|
712
709
|
| { type: "disconnect" }
|
|
713
710
|
| { type: "connect-back"; dialId: DialId }
|
|
714
|
-
| { type: "transfer"; action: "cold"; dialId: DialId;
|
|
715
|
-
| { type: "transfer"; action: "warm"; dialId: DialId;
|
|
711
|
+
| { type: "transfer"; action: "cold"; dialId: DialId; destinationId: string }
|
|
712
|
+
| { type: "transfer"; action: "warm"; dialId: DialId; destinationId: string }
|
|
716
713
|
| { type: "transfer"; action: "complete" }
|
|
717
714
|
| { type: "transfer"; action: "cancel" }
|
|
718
715
|
| { type: "lead-assist"; action: "request"; note?: string }
|
|
719
716
|
| { type: "lead-assist"; action: "cancel" }
|
|
720
717
|
| { type: "lead-assist"; action: "take-over" }
|
|
721
718
|
| { type: "lead-assist"; action: "leave" }
|
|
722
|
-
| { type: "conference"; action: "add"; dialId: DialId;
|
|
723
|
-
| { type: "conference"; action: "remove";
|
|
719
|
+
| { type: "conference"; action: "add"; dialId: DialId; destinationId: string }
|
|
720
|
+
| { type: "conference"; action: "remove"; destinationId: string }
|
|
724
721
|
| { type: "recording"; action: "start" | "pause" | "resume" | "stop" }
|
|
725
722
|
| ({ type: "complete" } & DispositionPayload);
|
|
726
723
|
|
|
@@ -1006,7 +1003,7 @@ type ProviderEvent =
|
|
|
1006
1003
|
| { type: "task-media-started"; taskId: TaskId }
|
|
1007
1004
|
| { type: "task-media-ended"; taskId: TaskId }
|
|
1008
1005
|
| { type: "task-ended"; taskId: TaskId; outcome: TaskOutcome }
|
|
1009
|
-
| { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome; taskId?: TaskId;
|
|
1006
|
+
| { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome; taskId?: TaskId; destinationId?: string; reason?: string }
|
|
1010
1007
|
| { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
|
|
1011
1008
|
| { type: "queue-summary"; summary: QueueSummary }
|
|
1012
1009
|
| { type: "diagnostic"; expected: string; observed: string; taskId?: TaskId }
|
|
@@ -2161,7 +2158,7 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2161
2158
|
| `wrapAllowance` | Fixed time allowed to complete the task after primary handling ends. For real-time media, it begins after `task-media-ended`. Required under `provider-automatic`, where the provider acts on it. Optional under `agent-command`: omitted says the provider imposes no deadline, and Omni counts nothing down. |
|
|
2162
2159
|
| `attributes` | Optional ordered, typed `TaskAttribute` entries with keys unique within the task. Each contact or timestamp is a separate array item; new attribute shapes require new union members. |
|
|
2163
2160
|
| `handlingHistory` | The call record: `steps` — the ordered handling history of this open task, one entry per occurrence, oldest first — and what they add up to before this agent, `handleSeconds`, `holdSeconds`, `queueSeconds`, `transfers`, each present when the provider knows it. Live task data restated with the task, not a permanent archive. See **How a task has been handled**. |
|
|
2164
|
-
| `onCall` | Voice only. Who is on the call, or being brought onto it, as the provider states it, replaced whole with the task: `party` is the customer, `agent` a person by user id, `consulted` and `conferenced` somebody a dial is bringing in, listed from the moment the dial is placed -- with the `
|
|
2161
|
+
| `onCall` | Voice only. Who is on the call, or being brought onto it, as the provider states it, replaced whole with the task: `party` is the customer, `agent` a person by user id, `consulted` and `conferenced` somebody a dial is bringing in, listed from the moment the dial is placed -- with the `destinationId` dialled, the `dialId` where a host placed it, the `stage` reached (`ringing` until answered, `joined` after), and `held: true` on anyone joined and parked. A `consulted` entry is what makes `transfer` `complete` and `cancel` issuable. `label` names a destination -- a person, a queue -- not a phrase; the host supplies the verb. Present when the provider knows the room, absent when it does not. See **Every dial has an outcome**. |
|
|
2165
2162
|
| `leadAssist` | Voice only. Present from the agent's request for a lead until the lead leaves or the request ends: `requested` while nobody has joined, `joined` with the lead's `leadId` once somebody has. See **Lead assist**. |
|
|
2166
2163
|
| `assisting` | Voice only, on the lead's own task for a call they joined: which member asked, with their note. Its presence is what makes `lead-assist` `take-over` and `leave` issuable. See **Lead assist**. |
|
|
2167
2164
|
| `monitoring` | Voice only, on the lead's own task while they listen to a member's call: whose call, which call, and the `mode` they are heard in, restated on every change. Never on the member's task, and never together with `assisting`. See **Monitoring a call**. |
|
|
@@ -2414,7 +2411,7 @@ executes** — so without a step the provider, which alone keeps the task's reco
|
|
|
2414
2411
|
account of a period the agent could not be heard.
|
|
2415
2412
|
|
|
2416
2413
|
**A step that dialled says which dial and where.** `transferred`, `conferenced` and `unanswered`
|
|
2417
|
-
each end a dial, so the entry carries the `
|
|
2414
|
+
each end a dial, so the entry carries the `destinationId` it went to and, where a host placed it, the
|
|
2418
2415
|
`dialId` the host minted; no other step dialled, and either field on one is refused
|
|
2419
2416
|
(`task.handlingHistory.dialId.unexpected`). This is how a dial made before a transfer is placeable
|
|
2420
2417
|
by whoever holds the task now: a `dial-outcome` naming a `dialId` the host never minted is looked
|
|
@@ -2674,9 +2671,9 @@ the provider published none.
|
|
|
2674
2671
|
```ts
|
|
2675
2672
|
capabilities: {
|
|
2676
2673
|
coldTransfer: {
|
|
2677
|
-
allowManualEntry: false,
|
|
2678
2674
|
destinations: [
|
|
2679
|
-
{ id: "tier2", label: "Tier 2 support"
|
|
2675
|
+
{ id: "tier2", label: "Tier 2 support" },
|
|
2676
|
+
{ id: "main-menu", label: "Back to the main menu" },
|
|
2680
2677
|
],
|
|
2681
2678
|
},
|
|
2682
2679
|
}
|
|
@@ -2684,22 +2681,23 @@ capabilities: {
|
|
|
2684
2681
|
|
|
2685
2682
|
| Field | Contract |
|
|
2686
2683
|
| --- | --- |
|
|
2687
|
-
| `destinations` |
|
|
2688
|
-
| `
|
|
2689
|
-
| `
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2684
|
+
| `destinations` | The items the control offers, at least one, with unique `id` values. Each is a button or a menu item on the desk. |
|
|
2685
|
+
| `id` | What Omni sends as `destinationId` on a `transfer` or `conference` command, and what the provider executes. |
|
|
2686
|
+
| `label` | What the agent reads. |
|
|
2687
|
+
|
|
2688
|
+
**The protocol does not say what an item does.** A queue, an IVR menu, an outside line, a
|
|
2689
|
+
skill group -- that is the queue's configuration and the provider's business. The agent sees a
|
|
2690
|
+
list of labels, picks one, and Omni sends its `id`; the provider executes whatever the item is. No
|
|
2691
|
+
kind travels, nothing is typed, and a control with an empty directory has nothing to offer and is
|
|
2692
|
+
refused (`task.destinations.offer`). A control declared as bare `true` is refused for the same
|
|
2693
|
+
reason (`task.destinations.shape`): once nothing is typed, the directory is the control.
|
|
2694
|
+
|
|
2695
|
+
**An agent never transfers to a named agent**, warm or cold. A contact that needs another pair of
|
|
2696
|
+
hands goes back to a queue or a menu the queue configured, and who takes it next is the queue's
|
|
2697
|
+
decision: routing by skill, availability and fairness is what the queue is for, and a hand-off to a
|
|
2698
|
+
chosen colleague bypasses all three. The desk shows no agent picker and no dial pad on a transfer.
|
|
2699
|
+
The one way a named person ends up with an agent's call is a lead taking it over through lead
|
|
2700
|
+
assist, which is the lead's act, not a transfer the agent chose.
|
|
2703
2701
|
|
|
2704
2702
|
#### Warm transfer
|
|
2705
2703
|
|
|
@@ -2713,7 +2711,7 @@ offer any of the three without the others, and each is declared on its own.
|
|
|
2713
2711
|
// 1. Warm. A dial: the provider parks the customer and calls the destination, answers
|
|
2714
2712
|
// `dialling`, and the task reports `paused` with the destination `consulted` on `onCall`
|
|
2715
2713
|
// while the call to it stands.
|
|
2716
|
-
{ type: "transfer", action: "warm", dialId: "dial-7f2",
|
|
2714
|
+
{ type: "transfer", action: "warm", dialId: "dial-7f2", destinationId: "tier2" }
|
|
2717
2715
|
|
|
2718
2716
|
// 2a. Hand the customer to the consulted destination and leave.
|
|
2719
2717
|
{ type: "transfer", action: "complete" }
|
|
@@ -2722,7 +2720,7 @@ offer any of the three without the others, and each is declared on its own.
|
|
|
2722
2720
|
{ type: "transfer", action: "cancel" }
|
|
2723
2721
|
```
|
|
2724
2722
|
|
|
2725
|
-
`warm` is gated by the `warmTransfer` capability and
|
|
2723
|
+
`warm` is gated by the `warmTransfer` capability and names a directory item exactly as a cold
|
|
2726
2724
|
transfer does, from the same kind of directory. It is a dial, so it carries the host's `dialId`
|
|
2727
2725
|
and is answered `dialling`, and its `dial-outcome` says whether the destination was reached. While
|
|
2728
2726
|
the destination is on the line the task carries a `consulted` entry on `onCall`, and that presence
|
|
@@ -2832,16 +2830,16 @@ declare const connection: Connection<"voice">;
|
|
|
2832
2830
|
declare const taskId: TaskId;
|
|
2833
2831
|
|
|
2834
2832
|
// 1. Out, minted by the host.
|
|
2835
|
-
const result = await connection.execute({ taskId, command: { type: "conference", action: "add", dialId: "dial-7f2",
|
|
2833
|
+
const result = await connection.execute({ taskId, command: { type: "conference", action: "add", dialId: "dial-7f2", destinationId: "tier2" } });
|
|
2836
2834
|
|
|
2837
2835
|
// 2. Back on the result, restated, so the host compares and confirms.
|
|
2838
2836
|
expect(result).toEqual({ status: "dialling", dialId: "dial-7f2" });
|
|
2839
2837
|
|
|
2840
2838
|
// 3. On the outcome, however late, against the task it belonged to.
|
|
2841
|
-
const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2", taskId,
|
|
2839
|
+
const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2", taskId, destinationId: "tier2", outcome: "no-answer", reason: "No route to destination" };
|
|
2842
2840
|
|
|
2843
2841
|
// 4. In the record, so a dial made before a transfer is placeable by whoever holds the task now.
|
|
2844
|
-
const step: TaskHandlingStep = { step: "unanswered", at: "2026-08-21T09:15:30Z", by: "A-12", dialId: "dial-7f2",
|
|
2842
|
+
const step: TaskHandlingStep = { step: "unanswered", at: "2026-08-21T09:15:30Z", by: "A-12", dialId: "dial-7f2", destinationId: "tier2" };
|
|
2845
2843
|
```
|
|
2846
2844
|
|
|
2847
2845
|
The identity is a correlation handle between host and provider and **is never shown to the
|
|
@@ -2857,7 +2855,7 @@ nothing, is refused (`result.status`). `validateResult(result, method, path, dia
|
|
|
2857
2855
|
`dialId` the host sent for that reason.
|
|
2858
2856
|
|
|
2859
2857
|
**The outcome is stated, once, either way.** A `dial-outcome` names the `dialId`, the task where
|
|
2860
|
-
there was one, the
|
|
2858
|
+
there was one, the directory item it went to where there was one, and how the dial ended -- from a closed
|
|
2861
2859
|
set: `answered`, `busy`, `no-answer`, `unreachable`, `rejected`, `cancelled`, `unexplained`.
|
|
2862
2860
|
`answered` is stated rather than read off somebody appearing on the call, because a host that infers
|
|
2863
2861
|
success cannot tell "reached" from "still ringing". `cancelled` is the dialler calling the dial off
|
|
@@ -2910,15 +2908,15 @@ knows the room:
|
|
|
2910
2908
|
const onCall: OnCall[] = [
|
|
2911
2909
|
{ role: "party", since: "2026-08-21T09:12:00Z" },
|
|
2912
2910
|
{ role: "agent", userId: "A-12", since: "2026-08-21T09:12:04Z" },
|
|
2913
|
-
{ role: "conferenced",
|
|
2914
|
-
{ role: "conferenced",
|
|
2911
|
+
{ role: "conferenced", destinationId: "tier2", dialId: "dial-7f2", stage: "joined", held: true, since: "2026-08-21T09:15:30Z" },
|
|
2912
|
+
{ role: "conferenced", destinationId: "main-menu", dialId: "dial-3c9", stage: "ringing", since: "2026-08-21T09:16:02Z" },
|
|
2915
2913
|
];
|
|
2916
2914
|
```
|
|
2917
2915
|
|
|
2918
2916
|
`party` is the customer and `agent` a person by user id; neither was dialled from anywhere, so
|
|
2919
|
-
neither carries a destination. `consulted` and `conferenced` were, so both carry the
|
|
2920
|
-
and, where a host's dial brought them, its `dialId`; a person
|
|
2921
|
-
none. `held: true` is presence as claim. A snapshot establishes state, so a host that attaches
|
|
2917
|
+
neither carries a destination. `consulted` and `conferenced` were, so both carry the
|
|
2918
|
+
`destinationId` of the directory item and, where a host's dial brought them, its `dialId`; a person
|
|
2919
|
+
the platform added itself carries none. `held: true` is presence as claim. A snapshot establishes state, so a host that attaches
|
|
2922
2920
|
after a transfer reads the room from here rather than inferring it from a sequence of outcomes it
|
|
2923
2921
|
never saw.
|
|
2924
2922
|
|
|
@@ -2927,7 +2925,7 @@ what makes `transfer` `cancel` issuable and what `conference` `remove` names, an
|
|
|
2927
2925
|
agent cannot call off while it rings is a parked customer with no way back -- which is the very case
|
|
2928
2926
|
`cancelled` exists for. Consult and conference behave alike here: the `consulted` or `conferenced`
|
|
2929
2927
|
entry appears with its `dialId` when the dial is placed, and a `conference` `remove` naming a
|
|
2930
|
-
still-ringing `
|
|
2928
|
+
still-ringing `destinationId` calls that dial off, with `cancelled` as its outcome.
|
|
2931
2929
|
|
|
2932
2930
|
**The entry says where it stands.** A dialled entry carries `stage`: `ringing` until its dial is
|
|
2933
2931
|
answered, `joined` after. The `dial-outcome` is the transition and the stage is the state, the same
|
|
@@ -3806,7 +3804,7 @@ react rather than only display the message:
|
|
|
3806
3804
|
| `omni.not-authenticated` | The provider session is no longer usable. The adapter has published `expired` at or before this answer — the state is what Omni surfaces reauthentication from; the code says why this action failed, and is never the only signal. |
|
|
3807
3805
|
| `omni.capability-not-enabled` | The action targets a capability this task, manifest, or login did not declare — including a lead command from a login whose `capabilities` no longer carry it. |
|
|
3808
3806
|
| `omni.task-not-found` | The provider-local task id is unknown, typically after the task already ended. |
|
|
3809
|
-
| `omni.destination-not-permitted` | The
|
|
3807
|
+
| `omni.destination-not-permitted` | The dialled number, or the `destinationId` named, is not one the provider offers this agent. |
|
|
3810
3808
|
| `omni.rate-limited` | The action was throttled. Pair with `retryAfterMs`. |
|
|
3811
3809
|
| `omni.unavailable` | The provider is temporarily unable to serve the action, including any command sent while `transport-status` is not `active`. |
|
|
3812
3810
|
| `omni.break-already-committed` | Cancellation lost the commit/cancel race; Omni must finish commit recovery. |
|